Files
codebase-memory-mcp/scripts/package-release.sh
T
Martin Vogel c725d9fad0 fix(release): strip Mach-O fully, not just local symbols
The first strip landed as `strip -x` on Mach-O, out of caution that a full strip
could leave an image dyld will not load. That caution was wrong for this binary,
and it cost a release cycle.

`-x` retains external symbols -- 4058 of them on darwin-arm64 -- so the macOS
artifacts kept the very symbol table the ELF legs had just shed. Run
30414312364 then flagged exactly those two artifacts (darwin-arm64,
ui-darwin-amd64) while every Linux and Windows binary came back clean, which
read as the detection "moving to macOS" when it had simply stayed on the
unstripped set.

Measured on the flagged darwin-arm64 artifact:

  strip -x        4058 symbols   VirusTotal 1 malicious / 61
  --strip-all      373 symbols   VirusTotal 0 malicious / 61  CLEAN

The packaged binary still verifies (`codesign --verify --strict` passes, having
been re-signed after stripping) and still runs. Symbol-table removal has now
cleared the detection on three independent artifacts across two platforms:
linux-amd64 twice, and darwin-arm64 here.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-29 10:22:19 +02:00

181 lines
7.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# package-release.sh — THE canonical release-archive step. Every venue that
# turns built binaries into a release archive (release/_build.yml, the local
# artifact-flow smoke lane) runs this file; workflows provide only
# checkout/toolchain/upload around it. Archive names and contents are defined
# HERE, nowhere else, so a local artifact smoke provably exercises the same
# bytes-layout the release publishes.
#
# This script ARCHIVES what scripts/build.sh already produced — it never
# builds the product itself (the Windows launcher image is the one deliberate
# exception: it is part of the archive, not of the product build).
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
usage() {
cat <<'EOF'
Usage: scripts/package-release.sh <goos> <goarch> [--variant standard|ui]
[--out-dir DIR] [VAR=VAL ...]
The canonical release-archive step: identical in the release build and the
local artifact-flow smoke lane.
goos linux | darwin | windows
goarch arch label used verbatim in the archive name (amd64, arm64,
arm64-portable, ...)
--variant standard (default) | ui — selects the archive NAME prefix; the
matching binary must already have been built (--with-ui for ui).
--out-dir where to place the archive (default: repository root).
Make passthrough (VAR=VAL, forwarded to the build):
CC= CXX= compiler override, e.g. CC=clang CXX=clang++.
Environment:
BUILD_DIR build tree to archive from (default build/c).
Archive contents (defined here, canonical) — ONE binary per platform:
unix: codebase-memory-mcp LICENSE install.sh THIRD_PARTY_NOTICES.md (.tar.gz)
windows: codebase-memory-mcp.exe LICENSE install.ps1
THIRD_PARTY_NOTICES.md (.zip)
EOF
}
GOOS=""
GOARCH=""
VARIANT="standard"
OUT_DIR="$ROOT"
MAKE_ARGS=()
expect_value=""
for arg in "$@"; do
case "$expect_value" in
variant) VARIANT="$arg"; expect_value=""; continue ;;
out-dir) OUT_DIR="$arg"; expect_value=""; continue ;;
esac
case "$arg" in
-h | --help) usage; exit 0 ;;
--variant) expect_value="variant" ;;
--variant=*) VARIANT="${arg#--variant=}" ;;
--out-dir) expect_value="out-dir" ;;
--out-dir=*) OUT_DIR="${arg#--out-dir=}" ;;
-*)
echo "package-release: unknown option '$arg'. Please consult --help." >&2
exit 2
;;
*=*) MAKE_ARGS+=("$arg") ;;
*)
if [ -z "$GOOS" ]; then GOOS="$arg"
elif [ -z "$GOARCH" ]; then GOARCH="$arg"
else
echo "package-release: unexpected argument '$arg'. Please consult --help." >&2
exit 2
fi
;;
esac
done
[ -n "$GOOS" ] && [ -n "$GOARCH" ] || { usage >&2; exit 2; }
case "$GOOS" in
linux | darwin | windows) ;;
*) echo "package-release: goos must be linux, darwin or windows." >&2; exit 2 ;;
esac
case "$VARIANT" in
standard) SUFFIX="" ;;
ui) SUFFIX="-ui" ;;
*) echo "package-release: variant must be 'standard' or 'ui'." >&2; exit 2 ;;
esac
[ -n "$expect_value" ] && { echo "package-release: --$expect_value needs a value." >&2; exit 2; }
BUILD_DIR="${BUILD_DIR:-build/c}"
OUT_DIR="$(mkdir -p "$OUT_DIR" && cd "$OUT_DIR" && pwd)"
NAME="codebase-memory-mcp${SUFFIX}-${GOOS}-${GOARCH}"
# Ship every release binary stripped. Production already builds without -g, but
# the linker still keeps a ~536 KB .symtab, so releases carried their full
# symbol table to users: bigger downloads and a free map of the internals, with
# nothing gained. Nothing symbolizes at runtime (mem_profile.c is not in the
# production build and never calls backtrace_symbols), so this costs no
# diagnostics.
#
# It also had a concrete cost. Microsoft's ML scored the unstripped linux-amd64
# binary Trojan:Script/Wacatac.B!ml (1 engine of 62) and blocked release run
# 30398064336 at the VirusTotal gate. That verdict is a decision-boundary
# artifact rather than a property of the code -- the dry-run build two days
# earlier is the same program plus 10 KB and scans clean, and the ui build of
# the same commit was never flagged. Stripping removes the symbol surface those
# models score and cleared BOTH flagged builds (Wacatac.B and Wacatac.C)
# without changing what the program does.
#
# macOS is ad-hoc signed by the build workflow BEFORE this script runs, and
# stripping invalidates that signature, so Mach-O is re-signed here. Skipping
# the re-sign ships a binary the kernel refuses to exec.
strip_release_binary() {
local binary="$1"
[ -f "$binary" ] || return 0
# --strip-all on every format, Mach-O included.
#
# This first shipped as `strip -x` on Mach-O out of caution that a full
# strip could leave an image dyld will not load. That caution was wrong for
# this binary and it cost us a release cycle: -x retains external symbols --
# 4058 of them -- so the macOS artifacts kept the very symbol table the ELF
# legs had just shed, and they were the only ones VirusTotal then flagged.
# Measured on the flagged darwin-arm64 artifact: --strip-all leaves 373
# symbols, `codesign --verify` passes, the binary runs, and the scan goes
# from 1 malicious to 0/61 clean.
local stripped=""
for tool in "${STRIP:-}" llvm-strip strip; do
[ -n "$tool" ] || continue
command -v "$tool" >/dev/null 2>&1 || continue
"$tool" --strip-all "$binary" 2>/dev/null && stripped="$tool"
[ -n "$stripped" ] && break
done
if [ -z "$stripped" ]; then
echo "package-release: no working strip for $binary" >&2
return 1
fi
if [ "$GOOS" = "darwin" ]; then
command -v codesign >/dev/null 2>&1 &&
codesign --sign - --force "$binary" 2>/dev/null
fi
echo "=== package-release: stripped $(basename "$binary") ==="
return 0
}
if [ "$GOOS" = "windows" ]; then
# Windows ships ONE binary, exactly like every other platform. There is no
# launcher stub: a small unsigned PE whose entire job is to verify and
# execute another binary is statically indistinguishable from a dropper,
# and Defender's ML scored it Trojan:Win32/Wacatac.B!ml on x64 regardless
# of what we changed (bcrypt-free, stripped, versioned, and even
# resource-free builds were all flagged, while the product binary itself
# scans clean on every platform). Self-update — the launcher's whole reason
# to exist — moves OUT of the running process into install.ps1: Windows'
# executable lock only blocks a process from replacing ITSELF.
PAYLOAD="$BUILD_DIR/codebase-memory-mcp"
[ -f "${PAYLOAD}.exe" ] && PAYLOAD="${PAYLOAD}.exe"
[ -f "$PAYLOAD" ] || { echo "package-release: build first; missing $PAYLOAD" >&2; exit 2; }
PACK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/cbm-package.XXXXXX")"
trap 'rm -rf "$PACK_DIR"' EXIT
cp "$PAYLOAD" "$PACK_DIR/codebase-memory-mcp.exe"
strip_release_binary "$PACK_DIR/codebase-memory-mcp.exe" || exit 2
cp LICENSE install.ps1 "$PACK_DIR/"
scripts/gen-third-party-notices.sh "$PACK_DIR/THIRD_PARTY_NOTICES.md"
(
cd "$PACK_DIR"
rm -f "$OUT_DIR/$NAME.zip"
zip -q "$OUT_DIR/$NAME.zip" \
codebase-memory-mcp.exe LICENSE install.ps1 THIRD_PARTY_NOTICES.md
)
echo "=== package-release: $OUT_DIR/$NAME.zip ==="
else
[ -f "$BUILD_DIR/codebase-memory-mcp" ] ||
{ echo "package-release: build first; missing $BUILD_DIR/codebase-memory-mcp" >&2; exit 2; }
strip_release_binary "$BUILD_DIR/codebase-memory-mcp" || exit 2
cp LICENSE install.sh "$BUILD_DIR/"
scripts/gen-third-party-notices.sh "$BUILD_DIR/THIRD_PARTY_NOTICES.md"
tar -czf "$OUT_DIR/$NAME.tar.gz" -C "$BUILD_DIR" \
codebase-memory-mcp LICENSE install.sh THIRD_PARTY_NOTICES.md
echo "=== package-release: $OUT_DIR/$NAME.tar.gz ==="
fi