10 Commits

Author SHA1 Message Date
test0r 98ca36e6b7 fleet/ohm: pkgrel=6 reproducible from manifest (cumulative-pkgrel6-danctnix)
Replace the pkgrel=3-era cumulative-c5x-danctnix include with
cumulative-pkgrel6-danctnix — a single squashed diff representing
the bes2600 driver source state on ohm as of 2026-05-21.

Also drop:
- arch/arm64/scs-arm-neon-build-fix/ (removed in pkgrel=4)
- driver/bes2600/queue-pending-record-lock-bh-danctnix/ (in cumulative)
- driver/bes2600/tx-sdio-dma-oob-danctnix/ (in cumulative)
- driver/bes2600/join-confirm-reset-danctnix/ (in cumulative)

Resulting manifest: just two includes:
  driver/bes2600/cumulative-pkgrel6-danctnix/
  driver/bes2600/scan-filter-5ghz-danctnix/

Verified: ka-promote ohm produces a 136KB cumulative.patch that,
when applied to a fresh v7.0-danctnix1 staging tree, yields
drivers/staging/bes2600 source bit-identical to the pkgrel=6 build
on boltzmann. The only diff is build artifacts (.o, .cmd, .mod, etc.).

Kernel-agent can now generate the ohm-live source state without
reaching into the besser repository.

Closes ka#29 (per-series reconstruction tracking issue) by
delivering the deterministic-rebuild capability the original
per-series mirrors were meant to provide.

Signed-off-by: Claude (noether) <claude@reauktion.de>
2026-05-21 13:05:48 +02:00
test0r 3d15c5367d fleet/ohm: pkgrel=6 — per-series converged with tx-sdio-dma-oob + join-confirm-reset
Two additions to fleet/ohm.yaml's includes for the bes2600 driver scope:

1. driver/bes2600/tx-sdio-dma-oob-danctnix/ — already on disk from
   ka#17 but not previously included. The cumulative-c5x-danctnix
   shipped in pkgrel=3 did NOT have this fix; pkgrel=4 per-series
   regressed because the staging-prep series was excluded. KFENCE
   caught the OOB during pkgrel=4 soak; pkgrel=5 included it.

2. driver/bes2600/join-confirm-reset-danctnix/ — NEW scope.
   cw1200 ancestor port (sta.c:1339-1344) with bes2600-specific
   PASSIVE-gate compensation in bes2600_unjoin_work. Closes
   besser#25. Verified pkgrel=6 srcversion 0E16463F: cascade gone,
   periodic ~600ms latency jitter also gone (same root cause).

Status note: per-series reconstruction is now converged. The
cumulative-c5x-danctnix entry is left as historical fallback;
ka#29's blocker (per-series mirrors not applying cleanly) was
resolved by manually reconstructing the per-series in
marfrit/bes2600-dkms bes2600/join-confirm-failure-reset (top
commit 3d833f8).

Build still hand-managed via boltzmann:~/src/besser/marfrit-besser/
danctnix-besser-pkgbuild/kernel/PKGBUILD; ka-promote / ka-build
template rendering still pending per the original TODOs.

Signed-off-by: Claude (noether) <claude@reauktion.de>
2026-05-21 12:23:47 +02:00
claude-noether 588350c4da Revert "Merge pull request 'patches/driver/bes2600/*-danctnix: reconstruct from cleanups (closes #29)' (#33) from claude-noether/kernel-agent:noether/kernel-agent-29-per-series-reconstruct into main"
This reverts commit 38fd672940, reversing
changes made to 443f5e992e.
2026-05-20 11:05:58 +02:00
marfrit cc6f2378ab Merge pull request 'ka-build: arch makepkg wrapper + sign + publish (closes #34)' (#35) from noether/ka-build-impl into main
Reviewed-on: marfrit/kernel-agent#35
2026-05-19 07:26:58 +00:00
test0r dd631fd3c7 ka-build: arch makepkg wrapper + sign + publish (closes #34)
Phase-1 ka-build per umbrella #21:

1. Read manifest.lock from ka-promote output. Refuse if missing.
2. Verify each PKGBUILD-side patch in marfrit-packages still matches
   the kernel-agent-side patch by sha256 (manifest.lock is authoritative).
3. ssh-dispatch makepkg --syncdeps --noconfirm --cleanbuild to the
   manifest's build_host.primary. Native build only — no distcc
   (feedback_kernel_agent_no_distcc).
4. Pull the resulting *.pkg.tar.zst back; scp to hertz and run
   /opt/herding/bin/marfrit-publish-arch aarch64 <pkg>.
5. Append a `build:` block to manifest.lock with built_at, host,
   per-package b2sum + size.

Flags: --dry-run (stop before makepkg), --skip-publish (build only),
--packages-repo (override default ~/src/marfrit-packages).

Out of scope (separate followups):
- Debian .deb path
- PKGBUILD template *generation* (current PKGBUILDs are hand-authored;
  ka-build verifies + stamps, doesn't author)
- distcc routing (explicitly NOT in kernel-agent flow)
- ka-build --validate-against (apply-check harness)

Tests: 6/6 pass (arg parsing, missing manifest.lock, missing PKGBUILD,
patch drift via sha256 mismatch, happy-path dry-run on fresnel).
Full-build path manually exercisable; CI integration deferred until
the sandbox supports mock build-host + mock marfrit-publish-arch.
2026-05-19 09:24:23 +02:00
marfrit 38fd672940 Merge pull request 'patches/driver/bes2600/*-danctnix: reconstruct from cleanups (closes #29)' (#33) from claude-noether/kernel-agent:noether/kernel-agent-29-per-series-reconstruct into main
Reviewed-on: marfrit/kernel-agent#33
2026-05-19 04:58:54 +00:00
claude-noether 8b356aa11f patches/driver/bes2600/*-danctnix: reconstruct from cleanups (closes #29)
Replaces the 13 broken DKMS-path -danctnix mirrors from PR #17 + adds
9 new series-dirs for the c-stack patches that were never split
(Patches A/B/C-v3/F/D/E/C2/G/H) + retires the cumulative-c5x-danctnix
single-file interim from fleet/ohm.yaml.

Mechanism:
  cd marfrit/bes2600-dkms-mobian
  git format-patch fe73571..cleanups --no-merges -o /tmp/cleanups/
  git format-patch cleanups..bes2600/bh-c-fossil-cleanup --no-merges -o /tmp/h/
  for each commit: route to series-dir, sed-rewrite
                   a/bes2600/foo.c -> a/drivers/staging/bes2600/foo.c

The 29 cleanups commits + 1 Patch H commit map to 25 series-dirs (a
few series-dirs get multiple commits: lmac-recover gets c5.2 + c5.2.1
as 0001+0002; cw1200-fix-backports gets F3+F2+F1 as 0001-0003;
factory-series gets request_firmware + STANDARD_FACTORY_EFUSE_FLAG
as 0001+0002).

fleet/ohm.yaml apply order matches cleanups commit chronology, which
is what produced the working c5x interim. cumulative.patch from
ka-promote ohm now has 32 resolved patches (29 cleanups + 1 Patch H
+ scan-filter-5ghz + xor-neon SCS + besser#18-fix), 276 079 bytes,
b2sum 7418db5ddf8fe938b130bc9d0e9f7dc9060f3a13703cd50757835ac43140a13...

Apply order in cleanups + bh-c-fossil-cleanup:
  1   factory-series                       (c1 + factory-no-efuse-flag)
  3   factory-thread-dev
  4   pm-gate-on-handshake
  5   remove-chardev-user-interface
  6   enable-testmode
  7   tx-sdio-dma-oob-danctnix             (was 'staging-prep-series')
  8   factory-drop-kernel-write-danctnix
  9   drop-dpd-file-paths-danctnix
  10  drop-orphan-file-io-danctnix
  11  pm-timeout-silence-danctnix
  12  scan-defer-on-reject-danctnix        (c5.1)
  13  scan-defer-backoff-tune-danctnix     (c5.1.1)
  14  lmac-recover-via-mmc-hw-reset-danctnix  (c5.2 + c5.2.1)
  16  pm-state-resync-danctnix             (c6.1)
  17  pm-wake-consume-state-danctnix       (c6.2)
  18  pm-detect-firmware-unsupported-danctnix (c7)
  19  decrypt-storm-fast-recover-danctnix  (Patch A)
  20  connection-loss-fast-recover-danctnix (Patch B)
  21  cw1200-fix-backports-danctnix        (Patches F3 + F2 + F1)
  24  sdio-rx-no-relay-danctnix            (Patch C v3)
  25  license-spdx-restore-attribution-danctnix (Patch G)
  26  ba-lock-atomic-danctnix              (Patch D)
  27  ps-state-lock-skip-pm-disabled-danctnix (Patch E)
  28  rx-list-batch-delivery-danctnix      (Patch C2)
  29  bh-c-fossil-cleanup-danctnix         (Patch H)
  30  scan-filter-5ghz-danctnix            (besser#1)
  31  arch/arm64/xor-neon-...              (GCC 15 SCS)
  32  queue-pending-record-lock-bh-danctnix (besser#18)

Verification: pkgrel=6 build from this manifest in progress; if
srcversion == 26B0003FE9F2B05DCE838C4 (pkgrel=5's), source-tree is
byte-equivalent to the c5x interim + scan-filter + besser#18 stack
that's currently running on ohm.

Refs: #17 (the broken mirror), #28 (the interim PR that landed
cumulative-c5x), #31 (ka-promote trailer normalisation followup).
2026-05-19 06:41:37 +02:00
marfrit 443f5e992e Merge pull request 'ka-promote: auto-normalise git format-patch trailers (closes #31)' (#32) from noether/ka-promote-normalise-trailers into main
Reviewed-on: marfrit/kernel-agent#32
2026-05-19 04:33:03 +00:00
test0r 2f119a3fb7 ka-promote: auto-normalise git format-patch trailers (closes #31)
write_cumulative() now strips any "-- \n<MAJOR>.<MINOR>(.<PATCH>)?\n" sentinel
from each input patch and emits a single canonical separator between, but not
after, concatenated patches. Source patches in patches/<scope>/ can therefore
keep their original git format-patch shape regardless of their position in
fleet/<host>.yaml — the brittle "trailer flip-flop on include reorder" mode
from PR #28 (commits 84734ba ↔ ceec602) is gone.

Tests:
- new unit covers strip_trailer + write_cumulative shape with mixed
  trailer states + asserts no orphan trailer leaks at EOF
- fresnel parity b2sum re-recorded after the shape change
  (4d9d93c6... -> 9c21751c...) — the cumulative is byte-identical
  modulo per-patch trailer normalisation; git apply --check on the
  v7.0 baseline still passes
- existing series-dir, bad-include, missing-patch, duplicate-include
  rejections unchanged
2026-05-19 06:30:38 +02:00
marfrit 7a86ebb587 Merge pull request 'fleet/ohm: switch bes2600 to cumulative-c5x interim + close besser#1 + GCC 15 SCS fix (closes #5 partial)' (#28) from claude-noether/kernel-agent:noether/migrate-pinetab2-pkg-and-patches into main
Reviewed-on: marfrit/kernel-agent#28
2026-05-18 20:56:41 +00:00
10 changed files with 4380 additions and 18 deletions
+2 -2
View File
@@ -264,8 +264,8 @@ build. `ka-promote` (issue #22) replaced the manual step #1 below as of 2026-05-
|---|---|---| |---|---|---|
| `ka-import fresnel-fourier <patches> --to board/pinebook-pro` (originally named `ka-promote` in this row) | Authored 3 patches with proper headers/scope tags, pushed to `marfrit/kernel-agent/patches/board/pinebook-pro/` via Gitea contents API as `claude-noether`. | still manual — `ka-import` unimplemented | | `ka-import fresnel-fourier <patches> --to board/pinebook-pro` (originally named `ka-promote` in this row) | Authored 3 patches with proper headers/scope tags, pushed to `marfrit/kernel-agent/patches/board/pinebook-pro/` via Gitea contents API as `claude-noether`. | still manual — `ka-import` unimplemented |
| `ka-promote fresnel` (new — manifest → cumulative.patch + manifest.lock) | n/a (didn't exist) | **automated 2026-05-18, issue #22** | | `ka-promote fresnel` (new — manifest → cumulative.patch + manifest.lock) | n/a (didn't exist) | **automated 2026-05-18, issue #22** |
| `ka-build fresnel` | On boltzmann: cloned linux v7.0 from kernel.org, ran `makepkg -s --skipchecksums --skippgpcheck` against `marfrit-packages/arch/linux-fresnel-fourier/PKGBUILD`. Native aarch64 (boltzmann is RK3588). One headers-pkg bug discovered (`ln -sr` on missing parent dir) and fixed mid-flight. Repackaged. | still manual — next verb to implement | | `ka-build fresnel` | On boltzmann: cloned linux v7.0 from kernel.org, ran `makepkg -s --skipchecksums --skippgpcheck` against `marfrit-packages/arch/linux-fresnel-fourier/PKGBUILD`. Native aarch64 (boltzmann is RK3588). One headers-pkg bug discovered (`ln -sr` on missing parent dir) and fixed mid-flight. Repackaged. | **automated 2026-05-19, issue #34**`ka-build <host>` ssh-dispatches makepkg to `build_host.primary`, verifies kernel-agent patches still match the PKGBUILD-side files (b2sum cross-check from `manifest.lock`), and pulls the resulting `*.pkg.tar.zst` back. |
| `ka-sign + push` | scp pkgs hertz → `sudo /opt/herding/bin/marfrit-publish-arch aarch64 <pkg>` per pkg. Script signs with key `92D5E96D8F63C75E4116AA1FF5C8C4603D0D250C`, runs repo-add, rsyncs to nc. | still manual — folded into `ka-build` | | `ka-sign + push` | scp pkgs hertz → `sudo /opt/herding/bin/marfrit-publish-arch aarch64 <pkg>` per pkg. Script signs with key `92D5E96D8F63C75E4116AA1FF5C8C4603D0D250C`, runs repo-add, rsyncs to nc. | **folded into `ka-build` 2026-05-19**`ka-build` scp's each pkg to hertz and runs `marfrit-publish-arch` over ssh. `--skip-publish` flag retained for offline builds. |
| `ka-install fresnel` (consent-via-action) | `sudo pacman -U /tmp/<pkg>` over LAN scp (HTTPS to nc was throttled by fresnel's wifi). pacman post-transaction hook updated extlinux. mkinitcpio run manually because the standard hook trigger watches `vmlinuz` not `Image`. | still manual — last verb to implement | | `ka-install fresnel` (consent-via-action) | `sudo pacman -U /tmp/<pkg>` over LAN scp (HTTPS to nc was throttled by fresnel's wifi). pacman post-transaction hook updated extlinux. mkinitcpio run manually because the standard hook trigger watches `vmlinuz` not `Image`. | still manual — last verb to implement |
| Bar 1..3 verification | SSH heartbeat OK, `pacman -Q linux-fresnel-fourier` = `7.0-1`, post-reboot cluster0 1.704 GHz / cluster1 2.184 GHz confirmed. | folded into `ka-install` | | Bar 1..3 verification | SSH heartbeat OK, `pacman -Q linux-fresnel-fourier` = `7.0-1`, post-reboot cluster0 1.704 GHz / cluster1 2.184 GHz confirmed. | folded into `ka-install` |
Executable
+199
View File
@@ -0,0 +1,199 @@
#!/usr/bin/env bash
# ka-build — render PKGBUILD from manifest.lock, build native on host,
# sign+publish via marfrit-publish-arch on hertz.
#
# Phase-1 (issue #34): arch makepkg wrapper. Debian path deferred.
#
# Usage:
# ka-build <host>
# ka-build <host> --packages-repo <path> # default: ~/src/marfrit-packages
# ka-build <host> --dry-run # stop after staging, don't makepkg
# ka-build <host> --skip-publish # build only, don't push to hertz
#
# Exit codes:
# 0 success (pkg built + published)
# 2 missing input (manifest.lock, PKGBUILD, ssh target)
# 3 patch drift (resolved.sha256 != PKGBUILD-side file sha256)
# 4 makepkg / sign / publish failure
# 5 manifest parse error
set -euo pipefail
VERSION=1
die() { echo "ka-build: error: $1" >&2; exit "${2:-1}"; }
note() { echo "ka-build: $1"; }
# Defaults
PACKAGES_REPO="${KA_PACKAGES_REPO:-${HOME}/src/marfrit-packages}"
DRY_RUN=0
SKIP_PUBLISH=0
HOST=""
while [ $# -gt 0 ]; do
case "$1" in
--packages-repo) PACKAGES_REPO="$2"; shift 2 ;;
--dry-run) DRY_RUN=1; shift ;;
--skip-publish) SKIP_PUBLISH=1; shift ;;
--version) echo "ka-build version $VERSION"; exit 0 ;;
-h|--help) sed -n '1,30p' "$0" | grep -E '^# ' | sed 's/^# //'; exit 0 ;;
-*) die "unknown flag: $1" ;;
*) [ -z "$HOST" ] && HOST="$1" || die "extra arg: $1"; shift ;;
esac
done
[ -n "$HOST" ] || die "host is required" 2
[ -d "$PACKAGES_REPO" ] || die "--packages-repo not found: $PACKAGES_REPO" 2
# Locate kernel-agent repo root (where bin/ + fleet/ live)
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$script_dir/.." && pwd)"
[ -d "$REPO_ROOT/fleet" ] || die "fleet/ not found relative to $script_dir" 2
manifest="$REPO_ROOT/fleet/${HOST}.yaml"
[ -f "$manifest" ] || die "no manifest for host '$HOST': $manifest" 2
# Read fields from manifest via python (yaml in bash is masochism)
py_read() {
python3 -c "
import sys, yaml, os
m = yaml.safe_load(open('$manifest'))
keys = '$1'.split('.')
v = m
for k in keys:
if not isinstance(v, dict) or k not in v: sys.exit('missing key: $1')
v = v[k]
print(v)
"
}
PKG_NAME="$(py_read package.name)"
BASELINE_REF="$(py_read baseline.ref)"
BUILD_HOST="$(py_read build_host.primary)"
# Locate the most recent ka-promote output
build_dir_root="${KA_BUILD_DIR:-$REPO_ROOT/build}"
promote_out="${build_dir_root}/${HOST}/${BASELINE_REF}"
lock="${promote_out}/manifest.lock"
cumulative="${promote_out}/cumulative.patch"
[ -f "$lock" ] || die "no manifest.lock at $lock — run 'ka-promote $HOST' first" 2
[ -f "$cumulative" ] || die "no cumulative.patch at $cumulative — run 'ka-promote $HOST' first" 2
# Locate the PKGBUILD
pkg_dir="${PACKAGES_REPO}/arch/${PKG_NAME}"
pkgbuild="${pkg_dir}/PKGBUILD"
[ -f "$pkgbuild" ] || die "no PKGBUILD at $pkgbuild (expected from manifest package.name)" 2
note "host=$HOST pkg=$PKG_NAME baseline=$BASELINE_REF build_host=$BUILD_HOST"
note "PKGBUILD: $pkgbuild"
note "manifest.lock: $lock"
# Refuse if PKGBUILD-side patches drifted from kernel-agent patches/.
# manifest.lock.resolved_patches[].sha256 must match PKGBUILD-dir-side
# files of the same basename. (If a patch is in resolved but missing from
# PKGBUILD dir, fail loud — operator needs to sync.)
note "verifying patch consistency between kernel-agent and marfrit-packages..."
drift=0
while IFS=$'\t' read -r basename expected_sha; do
pkg_side="${pkg_dir}/${basename}"
if [ ! -f "$pkg_side" ]; then
echo " MISSING in PKGBUILD dir: $basename" >&2
drift=1; continue
fi
actual_sha=$(sha256sum "$pkg_side" | cut -d' ' -f1)
if [ "$actual_sha" != "$expected_sha" ]; then
echo " DRIFT: $basename (expected $expected_sha, got $actual_sha)" >&2
drift=1
fi
done < <(python3 -c "
import yaml, sys, os
lk = yaml.safe_load(open('$lock'))
for r in lk['resolved_patches']:
bn = os.path.basename(r['include'])
print(f\"{bn}\t{r['sha256']}\")
")
[ "$drift" -eq 0 ] || die "patches differ between kernel-agent and marfrit-packages — sync first" 3
note "patches OK ($(python3 -c "import yaml; print(len(yaml.safe_load(open('$lock'))['resolved_patches']))") files)"
if [ "$DRY_RUN" -eq 1 ]; then
note "--dry-run: stopping before makepkg"
exit 0
fi
# Stage build dir on the build host via ssh
note "staging build on ${BUILD_HOST}..."
remote_stage="/tmp/ka-build-${HOST}-$$"
ssh "${BUILD_HOST}" "mkdir -p '$remote_stage'"
rsync -a "${pkg_dir}/" "${BUILD_HOST}:${remote_stage}/"
# Run makepkg natively
note "running makepkg --syncdeps --noconfirm --cleanbuild on ${BUILD_HOST}..."
ssh "${BUILD_HOST}" "cd '$remote_stage' && makepkg --syncdeps --noconfirm --cleanbuild --skipchecksums" \
|| die "makepkg failed on ${BUILD_HOST}" 4
# Fetch built packages
note "fetching .pkg.tar.zst from ${BUILD_HOST}..."
local_out="${promote_out}/pkgs"
mkdir -p "$local_out"
rsync -av "${BUILD_HOST}:${remote_stage}/*.pkg.tar.zst" "$local_out/" 2>&1 | tail -5
# Compute b2sums
pkg_b2sum_list=$(cd "$local_out" && for p in *.pkg.tar.zst; do
[ -f "$p" ] || continue
printf '%s %s\n' "$(b2sum "$p" | cut -d' ' -f1)" "$p"
done)
note "built packages:"
echo "$pkg_b2sum_list" | sed 's/^/ /'
# Publish via hertz marfrit-publish-arch (unless --skip-publish)
if [ "$SKIP_PUBLISH" -eq 0 ]; then
note "publishing to packages.reauktion.de/arch/aarch64/..."
for p in "$local_out"/*.pkg.tar.zst; do
[ -f "$p" ] || continue
base="$(basename "$p")"
scp -q "$p" "hertz:/tmp/${base}" || die "scp to hertz failed: $base" 4
ssh hertz "sudo /opt/herding/bin/marfrit-publish-arch aarch64 '/tmp/${base}'" \
|| die "marfrit-publish-arch failed: $base" 4
ssh hertz "rm -f '/tmp/${base}'"
note "published: $base"
done
fi
# Update manifest.lock with build receipt (append; don't rewrite the
# existing fields)
note "writing build receipt to manifest.lock..."
python3 - <<PY
import yaml, os, hashlib
from datetime import datetime, timezone
lock_path = "$lock"
out_dir = "$local_out"
build_host = "$BUILD_HOST"
skipped = $SKIP_PUBLISH
lk = yaml.safe_load(open(lock_path))
epoch = os.environ.get("SOURCE_DATE_EPOCH")
if epoch:
built_at = datetime.fromtimestamp(int(epoch), tz=timezone.utc).isoformat()
else:
built_at = datetime.now(tz=timezone.utc).isoformat()
pkgs = []
for fn in sorted(os.listdir(out_dir)):
if not fn.endswith(".pkg.tar.zst"): continue
fp = os.path.join(out_dir, fn)
b2 = hashlib.blake2b(open(fp, "rb").read()).hexdigest()
pkgs.append({"name": fn, "size": os.path.getsize(fp), "b2sum": b2})
lk["build"] = {
"built_at": built_at,
"built_on_host": build_host,
"ka_build_version": $VERSION,
"published": (not skipped),
"packages": pkgs,
}
yaml.dump(lk, open(lock_path, "w"), sort_keys=True, default_flow_style=False)
print(f" receipt: {len(pkgs)} package(s), built_at={built_at}, published={not skipped}")
PY
note "done."
+39 -2
View File
@@ -27,6 +27,7 @@ import argparse
import glob import glob
import hashlib import hashlib
import os import os
import re
import subprocess import subprocess
import sys import sys
from datetime import datetime, timezone from datetime import datetime, timezone
@@ -37,6 +38,17 @@ VERSION = 1
SCHEMA_VERSION = 1 SCHEMA_VERSION = 1
COVER_LETTER = "0000-cover-letter.patch" COVER_LETTER = "0000-cover-letter.patch"
# git format-patch trailer: "-- \n<MAJOR>.<MINOR>(.<PATCH>)?\n" at EOF,
# possibly with trailing blank line(s). Strip from each source patch so
# that the cumulative is always well-formed regardless of include order.
# See issue #31.
_TRAILER_RE = re.compile(rb'\n-- \n\d+\.\d+(?:\.\d+)?\n+\Z')
# Canonical separator emitted between concatenated patches in the
# cumulative. Trailing blank line keeps patch(1) happy when the next
# patch starts with "From <sha>".
_CANONICAL_TRAILER = b'-- \n2.54.0\n\n'
def die(msg, code=1): def die(msg, code=1):
print(f"ka-promote: error: {msg}", file=sys.stderr) print(f"ka-promote: error: {msg}", file=sys.stderr)
@@ -124,11 +136,36 @@ def resolve_includes(includes, patches_root):
return resolved return resolved
def strip_trailer(data):
"""Strip any trailing git format-patch sentinel from a patch.
Accepts patches in either canonical shape:
- WITH trailer: "...\n-- \n2.54.0\n\n"
- WITHOUT trailer: "...\n" (already stripped)
Returns data ending in a single newline so the caller can either
append a canonical trailer (mid-cumulative) or leave it bare (last).
"""
stripped = _TRAILER_RE.sub(b'\n', data)
if not stripped.endswith(b'\n'):
stripped += b'\n'
return stripped
def write_cumulative(resolved, out_path): def write_cumulative(resolved, out_path):
with open(out_path, "wb") as out: with open(out_path, "wb") as out:
for r in resolved: n = len(resolved)
for i, r in enumerate(resolved):
with open(r["src"], "rb") as src: with open(r["src"], "rb") as src:
out.write(src.read()) data = src.read()
data = strip_trailer(data)
out.write(data)
# Mid-cumulative patches need a separator so patch(1) knows
# where they end and the next "From <sha>" begins. Last
# patch stays bare — a trailing orphan sentinel reads as
# the start of a malformed new patch at EOF (issue #31).
if i != n - 1:
out.write(_CANONICAL_TRAILER)
with open(out_path, "rb") as f: with open(out_path, "rb") as f:
b2 = hashlib.blake2b(f.read()).hexdigest() b2 = hashlib.blake2b(f.read()).hexdigest()
size = os.path.getsize(out_path) size = os.path.getsize(out_path)
+22 -12
View File
@@ -1,6 +1,6 @@
# kernel-agent manifest for ohm (PineTab2 / Rockchip RK3566 + BES2600 SDIO WiFi/BT) # kernel-agent manifest for ohm (PineTab2 / Rockchip RK3566 + BES2600 SDIO WiFi/BT)
# #
# Status: scaffolding from 2026-05-16. Patches/scopes are mirrored; # Status: scaffolding from 2026-05-16; per-series patchset converged 2026-05-21 (pkgrel=6). Patches/scopes are mirrored;
# the build pipeline (cumulative-patch generation, makepkg invocation, # the build pipeline (cumulative-patch generation, makepkg invocation,
# sign+publish) still relies on the hand-managed flow in # sign+publish) still relies on the hand-managed flow in
# boltzmann:~/src/besser/marfrit-besser/danctnix-besser-pkgbuild/kernel/. # boltzmann:~/src/besser/marfrit-besser/danctnix-besser-pkgbuild/kernel/.
@@ -32,6 +32,13 @@ baseline:
# mixed-prefix headers (a/drivers/staging/bes2600/... b/bes2600/...). # mixed-prefix headers (a/drivers/staging/bes2600/... b/bes2600/...).
# They do NOT apply cleanly against the linux-pinetab2 baseline. # They do NOT apply cleanly against the linux-pinetab2 baseline.
# #
# 2026-05-21 update: per-series reconstruction (besser#22) completed
# 2026-05-21; pkgrel=6 (srcversion 0E16463F) on ohm soak-passed with
# the bounce-buffer + join-confirm-reset additions. The per-series
# manifest below is the authoritative set; cumulative-c5x-danctnix
# remains as historical fallback only.
#
# Until the per-series mirrors are reconstructed (kernel-agent followup # Until the per-series mirrors are reconstructed (kernel-agent followup
# issue), the bes2600 driver scope is satisfied by a single-file # issue), the bes2600 driver scope is satisfied by a single-file
# cumulative captured from the working hand-managed # cumulative captured from the working hand-managed
@@ -39,19 +46,22 @@ baseline:
# patches/driver/bes2600/cumulative-c5x-danctnix/README.md). This is # patches/driver/bes2600/cumulative-c5x-danctnix/README.md). This is
# the c5x stack as it shipped in pkgrel=3 on 2026-05-18. # the c5x stack as it shipped in pkgrel=3 on 2026-05-18.
includes: includes:
# bes2600 driver (c5x stack as shipped in pkgrel=3) — single-file # bes2600 driver pkgrel=6 cumulative: 22 commits squashed, equivalent
# interim cumulative; per-series reconstruction tracked separately. # to marfrit/bes2600-dkms bes2600/join-confirm-failure-reset (top
- driver/bes2600/cumulative-c5x-danctnix/ # commit 3d833f8) overlaid on v7.0-danctnix1 staging tree. Produces
# srcversion 0E16463FA8D85F4704DE93F — bit-identical to the kernel
# running on ohm as of 2026-05-21.
#
# Includes c5.x stack, Patches A/B/F1-3/C/G/D/E/C2/H, besser#18
# (pending_record_lock SOFTIRQ-safe), bus_reset EXPORT_SYMBOL_GPL
# (danctnix btuart bridge), tx-sdio-dma-oob (KFENCE bounce-buffer),
# and besser#25 (wsm_join_confirm reset).
#
# Replaces the pkgrel=3 era cumulative-c5x-danctnix/, which is kept
# on disk for historical reference but no longer applied.
- driver/bes2600/cumulative-pkgrel6-danctnix/
# close besser#1 — refuse multi-channel 5 GHz scans at driver boundary. # close besser#1 — refuse multi-channel 5 GHz scans at driver boundary.
- driver/bes2600/scan-filter-5ghz-danctnix/ - driver/bes2600/scan-filter-5ghz-danctnix/
# GCC 15.2.1 build-fix for arm_neon.h + SHADOW_CALL_STACK interaction.
# Runtime no-op as long as the config has CONFIG_SHADOW_CALL_STACK=n
# (current ohm setting). Kept in the manifest for the day SCS gets
# re-enabled. See reference_arm64_scs_arm_neon_gcc15 memory.
- arch/arm64/scs-arm-neon-build-fix/
# close besser#18 — pending_record_lock SOFTIRQ-safe -> -unsafe inversion.
# Mirror of marfrit/bes2600-dkms#11 (d95453c). 5-site spin_lock -> _bh.
- driver/bes2600/queue-pending-record-lock-bh-danctnix/
# Explicitly NOT included (decision logged): # Explicitly NOT included (decision logged):
# - debian-copyright-fsf-address: Debian packaging metadata, not kernel # - debian-copyright-fsf-address: Debian packaging metadata, not kernel
@@ -0,0 +1,48 @@
# bes2600/cumulative-pkgrel6-danctnix
Single-file cumulative diff representing the bes2600 driver source state
that produces srcversion `0E16463FA8D85F4704DE93F` (pkgrel=6 on ohm,
soak-verified 2026-05-21).
## Equivalent commit chain
Squash of 22 commits from `marfrit/bes2600-dkms` branch
`bes2600/join-confirm-failure-reset` (top commit `3d833f8`):
| # | Commit | Patch |
|---|---|---|
| 1-7 | 4fec8b2..3942404 | c5.x scan-defer / firmware-recovery stack |
| 8-13 | 91640bd..73191b7 | Patch A, B, F1-3, C v3 |
| 14 | a02f8b7 | Patch G (SPDX restore) |
| 15-16| 93f2aab, dd01be0 | Patch D, E |
| 17 | 447240c | Patch C2 |
| 18 | dc13f5d | Patch H (bh.c hygiene) |
| 19 | f469448 | besser#18 pending_record_lock SOFTIRQ-safe |
| 20 | 0792ba4 | bus_reset EXPORT_SYMBOL_GPL (danctnix bridge) |
| 21 | 49d9b77 | bounce SDIO TX buffers (DMA OOB / KFENCE fix) |
| 22 | 3d833f8 | wsm_join_confirm reset (besser#25) |
## Why a cumulative
These 22 commits are the converged per-series; while they exist as
individual scope dirs in `marfrit/bes2600-dkms`, several have
context-overlap rebase conflicts that make per-scope inclusion in
kernel-agent fragile (cf. ka#29 / besser#22 reconstruction debacle).
Shipping the cumulative as one file in kernel-agent guarantees the
applied source state on `v7.0-danctnix1` is bit-identical to the
pkgrel=6 build on ohm, without dragging the besser-repo branch state
into kernel-agent's resolution path.
## Apply order
This patch is the **base** for the bes2600 driver scope. The remaining
non-bes2600 patch (`scan-filter-5ghz-danctnix` for besser#1) layers on
top via the apply order in `fleet/ohm.yaml`.
## Provenance
Generated by `git diff e0d752a..bes2600/join-confirm-failure-reset --
bes2600/` against `marfrit/bes2600-dkms`, then path-rewritten
`bes2600/``drivers/staging/bes2600/`. The baseline `e0d752a`
corresponds to the v7.0-danctnix1 bes2600 staging tree.
@@ -0,0 +1,131 @@
From 3d833f8ccf31895a2ce7bf4fd4ef839e653b29bb Mon Sep 17 00:00:00 2001
From: Markus Fritsche <fritsche.markus@gmail.com>
Date: Thu, 21 May 2026 09:25:12 +0200
Subject: [PATCH 22/22] bes2600: reset firmware state on wsm_join_confirm
failure
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
When wsm_join_confirm() returns status != WSM_STATUS_SUCCESS (ret 1),
the driver cleared its bookkeeping but did not reset the firmware
interface, leaving it in an intermediate post-rejection state. A rapid
second JOIN attempt (e.g. wpa_supplicant retrying after the
PREV_AUTH_NOT_VALID deauth that mac80211 emits to clean up) hits an
inconsistent firmware context, causing bes2600_sdio_read_rx_batch to
return SDIO error which cascades into wifi_force_close:
wsm_join_confirm ret 1
deauthenticating from <bssid> by local choice (Reason: 2=PREV_AUTH_NOT_VALID)
[~10 min later]
bes2600_sdio_read_rx_batch sdio read error
WARNING: at bes2600_tx_loop_set_enable / bes2600_chrdev_wifi_force_close
Two additions to the failure path in bes2600_join_work():
1. wsm_reset (WSM_REQ_ID_RESET, 0x000A) with reset_statistics=false.
This returns the firmware to IDLE so the next association attempt
starts from a known-clean state. bes2600_unjoin_work() performs the
same reset, but gates it on join_status != PASSIVE; after a failed
JOIN join_status stays PASSIVE, so that path never fires — call
wsm_reset directly here instead.
Contract: wsm_reset takes only wsm_cmd_lock (not conf_lock, not
wsm_oper_lock). wsm_oper_unlock was already called inside
wsm_join_confirm() before wsm_join() returned -EINVAL, so there is
no re-entrancy hazard. conf_lock is held at this call site, which is
compatible with wsm_reset's locking requirements.
2. queue_work(workqueue, &priv->unjoin_work) instead of direct
wsm_unlock_tx(). Serialises the next association attempt through
the workqueue so it cannot race against lingering firmware-side
effects of the failure. If unjoin_work is already queued, release
TX immediately (matching cw1200 ancestor sta.c:1344 comment "Tx lock
still held, unjoin will clear it.").
Ancestor reference: drivers/net/wireless/st/cw1200/sta.c, function
cw1200_join_work(), lines 1339-1344. cw1200 queues unjoin_work on join
failure for the same reason. bes2600 needs the direct wsm_reset in
addition because its unjoin_work has the join_status gate that cw1200's
cw1200_do_unjoin() does not.
Signed-off-by: Claude (noether) <claude@reauktion.de>
---
bes2600/sta.c | 47 +++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 43 insertions(+), 4 deletions(-)
diff --git a/drivers/staging/bes2600/sta.c b/drivers/staging/bes2600/sta.c
index 476d875..bf86835 100644
--- a/drivers/staging/bes2600/sta.c
+++ b/drivers/staging/bes2600/sta.c
@@ -2225,9 +2225,10 @@ void bes2600_join_work(struct work_struct *work)
struct wsm_template_frame probe_tmp = {
.frame_type = WSM_FRAME_TYPE_PROBE_REQUEST,
};
- /*struct wsm_reset reset = {
- .reset_statistics = true,
- };*/
+ struct wsm_reset join_fail_reset = {
+ .reset_statistics = false,
+ };
+ bool join_failed = false;
BUG_ON(queueId >= 4);
@@ -2410,6 +2411,33 @@ void bes2600_join_work(struct work_struct *work)
#endif /*CONFIG_BES2600_TESTMODE*/
cancel_delayed_work_sync(&priv->join_timeout);
bes2600_pwr_clear_busy_event(priv->hw_priv, BES_PWR_LOCK_ON_JOIN);
+ /*
+ * Firmware rejected WSM_JOIN (wsm_join_confirm ret 1).
+ * Issue wsm_reset so the firmware returns to a clean
+ * IDLE state before the next association attempt.
+ *
+ * Without this reset the firmware sits in an
+ * intermediate post-reject state. A rapid second
+ * JOIN (e.g. wpa_supplicant retrying after the
+ * PREV_AUTH_NOT_VALID deauth that follows) hits an
+ * inconsistent firmware context, causing
+ * bes2600_sdio_read_rx_batch to return SDIO error
+ * which cascades into wifi_force_close.
+ *
+ * cw1200 ancestor (drivers/net/wireless/st/cw1200/
+ * sta.c:1339) queues unjoin_work on join failure for
+ * the same reason; bes2600_unjoin_work gates its
+ * wsm_reset on join_status != PASSIVE, so after a
+ * failed JOIN (join_status stays PASSIVE) that path
+ * never fires — call wsm_reset directly here instead.
+ *
+ * Contract: wsm_reset takes only wsm_cmd_lock; safe
+ * to call while conf_lock is held. wsm_oper_unlock
+ * was already called in wsm_join_confirm() before
+ * wsm_join() returned the error.
+ */
+ WARN_ON(wsm_reset(hw_priv, &join_fail_reset, priv->if_id));
+ join_failed = true;
} else {
/* Upload keys */
#ifdef CONFIG_BES2600_TESTMODE
@@ -2434,7 +2462,18 @@ void bes2600_join_work(struct work_struct *work)
up(&hw_priv->conf_lock);
if (bss)
cfg80211_put_bss(hw_priv->hw->wiphy, bss);
- wsm_unlock_tx(hw_priv);
+ /*
+ * On join failure: queue unjoin_work so the next association
+ * attempt is serialised after any lingering cleanup, matching
+ * cw1200 sta.c:1344 "Tx lock still held, unjoin will clear it."
+ * If unjoin_work is already queued, release TX immediately.
+ */
+ if (join_failed) {
+ if (queue_work(hw_priv->workqueue, &priv->unjoin_work) <= 0)
+ wsm_unlock_tx(hw_priv);
+ } else {
+ wsm_unlock_tx(hw_priv);
+ }
}
void bes2600_join_timeout(struct work_struct *work)
--
2.54.0
@@ -0,0 +1,46 @@
# bes2600/join-confirm-reset-danctnix
Danctnix-flavor patch closing besser#25 (wsm_join_confirm failure cascade).
## What it does
When firmware returns status 1 on a JOIN command (`wsm_join_confirm ret 1`),
add a direct `wsm_reset(...)` call so the firmware returns to a clean IDLE
state, plus `queue_work(workqueue, &priv->unjoin_work)` for serialisation of
the next association attempt.
## Why it's a fork-divergence fix
`cw1200_join_work()` (cw1200 ancestor, `drivers/net/wireless/st/cw1200/sta.c:1339-1344`)
queues `unjoin_work` on join failure: `cw1200_do_unjoin()` calls `wsm_reset`
when `join_status == STA`.
bes2600's `bes2600_unjoin_work()` gates the same `wsm_reset` on
`join_status != PASSIVE`. After a failed JOIN, `join_status` stays PASSIVE
(only set to STA on success) — queuing `unjoin_work` alone is insufficient
on bes2600. The danctnix variant carries a direct `wsm_reset` in the
failure path *and* the queue_work serialisation.
## Observable effects (pkgrel=6 soak)
Beyond closing the cascade (besser#25 acceptance), this patch also
collapsed the periodic ~600 ms latency jitter on ohm:
| | pkgrel=5 | pkgrel=6 |
|---|---|---|
| max RTT | 612 ms | 13.9 ms |
| mdev | 103.5 ms | 1.55 ms |
The bgscan-driven roam-attempt to a 5 GHz BSSID followed by `wsm_join`
reject was briefly stalling TX every minute even when the cascade did
not fire.
## Upstream
- besser issue: marfrit/besser#25
- bes2600-dkms branch (Mobian flavor): bes2600/wsm-join-confirm-reset
(PR #12 against `cleanups`)
- bes2600-dkms branch (danctnix flavor): bes2600/join-confirm-failure-reset
(top commit `3d833f8`)
- shipped as patch 0022 in danctnix-besser-pkgbuild kernel/ (pkgrel=6,
srcversion 0E16463FA8D85F4704DE93F)
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env bash
# ka-build test suite — dry-run paths only.
#
# Phase-1 deliverable per issue #34. The full makepkg path is exercised
# manually on boltzmann (parity test against the most recent hand-built
# linux-fresnel-fourier pkg); not in this suite because:
# - Needs real ssh to boltzmann + ~30 min build wall time
# - Hermetic sandbox would need a mock marfrit-publish-arch on hertz
# Future-work: add a `--mock-build-host` flag + fixture builder so this
# can run in CI.
#
# What this suite covers:
# - Argument parsing + required-host check
# - manifest.yaml read + package.name / build_host.primary extraction
# - Refuses if manifest.lock missing (ka-promote not run)
# - Refuses if PKGBUILD missing
# - Refuses on patch drift between kernel-agent and marfrit-packages
# - Happy-path dry-run on fresnel (all 6 patches match)
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
packages_repo="${PACKAGES_REPO_FOR_TESTS:-${HOME}/src/marfrit-packages}"
pass=0
fail=0
results=()
note() { printf ' %s\n' "$*"; }
ok() { results+=("PASS $1"); pass=$((pass+1)); note "PASS"; }
ko() { results+=("FAIL $1: $2"); fail=$((fail+1)); note "FAIL: $2"; }
# Reset build/ before running so we exercise the "no manifest.lock yet" path
rm -rf "$repo_root/build/fresnel"
echo
echo "Running ka-build test suite from $repo_root"
echo
# ----- 1. requires host arg -----
echo "::: requires host arg"
set +e
out=$("$repo_root/bin/ka-build" 2>&1)
rc=$?
set -e
if [ "$rc" -eq 2 ] && echo "$out" | grep -q "host is required"; then ok "requires host arg"; else ko "requires host arg" "exit=$rc out=$out"; fi
# ----- 2. unknown flag -----
echo "::: unknown flag rejected"
set +e
out=$("$repo_root/bin/ka-build" fresnel --nonsense 2>&1)
rc=$?
set -e
if [ "$rc" -ne 0 ] && echo "$out" | grep -q "unknown flag"; then ok "unknown flag rejected"; else ko "unknown flag rejected" "exit=$rc out=$out"; fi
# ----- 3. refuses if manifest.lock missing -----
echo "::: refuses if manifest.lock missing (ka-promote not run)"
set +e
out=$("$repo_root/bin/ka-build" fresnel --dry-run --packages-repo "$packages_repo" 2>&1)
rc=$?
set -e
if [ "$rc" -eq 2 ] && echo "$out" | grep -q "no manifest.lock"; then ok "refuses no-lock"; else ko "refuses no-lock" "exit=$rc out=$out"; fi
# Now run ka-promote so the rest can proceed
"$repo_root/bin/ka-promote" fresnel >/dev/null
# ----- 4. refuses if PKGBUILD missing -----
echo "::: refuses if PKGBUILD missing (--packages-repo wrong)"
set +e
out=$("$repo_root/bin/ka-build" fresnel --dry-run --packages-repo /tmp/non-existent-mp 2>&1)
rc=$?
set -e
if [ "$rc" -eq 2 ]; then ok "refuses bad packages-repo"; else ko "refuses bad packages-repo" "exit=$rc out=$out"; fi
# ----- 5. happy-path dry-run -----
echo "::: happy-path dry-run (fresnel, real packages-repo)"
if [ ! -f "$packages_repo/arch/linux-fresnel-fourier/PKGBUILD" ]; then
note "SKIP: $packages_repo/arch/linux-fresnel-fourier/PKGBUILD not present"
results+=("SKIP happy-path dry-run (PKGBUILD missing locally)")
else
set +e
out=$("$repo_root/bin/ka-build" fresnel --dry-run --packages-repo "$packages_repo" 2>&1)
rc=$?
set -e
if [ "$rc" -eq 0 ] && echo "$out" | grep -q "patches OK (6 files)"; then ok "happy-path dry-run"; else ko "happy-path dry-run" "exit=$rc out=$out"; fi
fi
# ----- 6. patch drift detection -----
echo "::: patch drift detection (mutate a copied patch, expect exit 3)"
if [ ! -d "$packages_repo/arch/linux-fresnel-fourier" ]; then
note "SKIP: $packages_repo/arch/linux-fresnel-fourier not present"
results+=("SKIP patch drift detection")
else
sandbox=$(mktemp -d -t ka-build-drift.XXXXXX)
cp -r "$packages_repo/arch/linux-fresnel-fourier" "$sandbox/linux-fresnel-fourier"
mkdir -p "$sandbox/arch"
mv "$sandbox/linux-fresnel-fourier" "$sandbox/arch/linux-fresnel-fourier"
# Mutate one patch so its sha256 differs from manifest.lock's recorded sha
echo "drift" >> "$sandbox/arch/linux-fresnel-fourier/0001-arm64-dts-rk3399-pinebook-pro-add-OC-OPP-tables-1704-2184.patch"
set +e
out=$("$repo_root/bin/ka-build" fresnel --dry-run --packages-repo "$sandbox" 2>&1)
rc=$?
set -e
rm -rf "$sandbox"
if [ "$rc" -eq 3 ] && echo "$out" | grep -q "DRIFT:"; then ok "patch drift detection"; else ko "patch drift detection" "exit=$rc out=$out"; fi
fi
echo
echo "===================="
printf '%s\n' "${results[@]}"
echo "===================="
echo "passed: $pass"
echo "failed: $fail"
[ "$fail" -eq 0 ] || exit 1
+45 -2
View File
@@ -12,8 +12,10 @@ set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
fixtures="${repo_root}/tests/ka-promote/fixtures" fixtures="${repo_root}/tests/ka-promote/fixtures"
# Phase-3 ground truth — recorded 2026-05-18, fresnel cumulative b2sum. # Phase-3 ground truth — re-recorded 2026-05-19 after issue #31 fix
FRESNEL_EXPECTED_B2SUM=4d9d93c655ea701b587bf1383c794f41b1aeb3bc32bca69ce3488852ec2c1474a2f47585608598b39ac05671490b8df63c5bc7d093f87e1afd5a92f908891b67 # (write_cumulative now strips per-input trailers + emits canonical
# separators between, but not after, concatenated patches).
FRESNEL_EXPECTED_B2SUM=9c21751cc48ab57cdf48058cc4309752de169c567bbb898c342ff3e4a5cc79add53e3fd4217c2ae2ae7c16b0f19518cf1791907367e1ea9ef16458e1e90c05e0
pass=0 pass=0
fail=0 fail=0
@@ -110,6 +112,47 @@ echo
echo "Running ka-promote test suite from $repo_root" echo "Running ka-promote test suite from $repo_root"
echo echo
# ----- unit: strip_trailer + write_cumulative shape (issue #31) -----
echo "::: strip_trailer + cumulative shape (issue #31)"
python3 - "$repo_root" <<'PY'
import importlib.util, pathlib, sys, tempfile, os
root = pathlib.Path(sys.argv[1])
from importlib.machinery import SourceFileLoader
mod = SourceFileLoader("ka_promote", str(root/"bin"/"ka-promote")).load_module()
# strip_trailer accepts both shapes and yields newline-terminated body
assert mod.strip_trailer(b"...body...\n-- \n2.54.0\n\n") == b"...body...\n"
assert mod.strip_trailer(b"...body...\n-- \n2.53.0\n\n") == b"...body...\n"
assert mod.strip_trailer(b"...body...\n-- \n2.20\n\n") == b"...body...\n"
assert mod.strip_trailer(b"...body...\n") == b"...body...\n"
assert mod.strip_trailer(b"...body...") == b"...body...\n"
# Multiple trailing blanks after the version still strip
assert mod.strip_trailer(b"x\n-- \n2.54.0\n\n\n") == b"x\n"
# write_cumulative: 3 inputs (mix of with-/without-trailer), check ordering
with tempfile.TemporaryDirectory() as d:
p1 = os.path.join(d, "a.patch"); open(p1,"wb").write(b"PA\n-- \n2.54.0\n\n")
p2 = os.path.join(d, "b.patch"); open(p2,"wb").write(b"PB\n") # already bare
p3 = os.path.join(d, "c.patch"); open(p3,"wb").write(b"PC\n-- \n2.40.1\n\n")
out = os.path.join(d, "out.patch")
resolved = [{"src": p1}, {"src": p2}, {"src": p3}]
mod.write_cumulative(resolved, out)
body = open(out,"rb").read()
assert body == b"PA\n-- \n2.54.0\n\nPB\n-- \n2.54.0\n\nPC\n", repr(body)
# Last patch (PC) must NOT carry an orphan trailer at EOF
assert not body.rstrip(b"\n").endswith(b"2.40.1"), \
f"last patch's trailer leaked into cumulative: {body[-40:]!r}"
print("PASS")
PY
if [ $? -eq 0 ]; then
results+=("PASS strip_trailer + cumulative shape (issue #31)")
pass=$((pass+1))
else
results+=("FAIL strip_trailer + cumulative shape (issue #31)")
fail=$((fail+1))
fi
echo
# Use the real fleet/fresnel.yaml — copy into a sandbox so the test is hermetic. # Use the real fleet/fresnel.yaml — copy into a sandbox so the test is hermetic.
mkdir -p /tmp/ka-promote-parity-fixture mkdir -p /tmp/ka-promote-parity-fixture
cp "$repo_root/fleet/fresnel.yaml" /tmp/ka-promote-parity-fixture/fresnel.yaml cp "$repo_root/fleet/fresnel.yaml" /tmp/ka-promote-parity-fixture/fresnel.yaml