Skip to content

Commit ef9131e

Browse files
author
suibianwanwan
committed
feat(install): bundle musl runtime for portable install on old-glibc Linux
x64 musl archives now carry the musl loader + libstdc++/libgcc_s under bin/lib/. setup.sh detects an old-glibc host (musl loader absent) and installs the real binary under libexec, rewrites its ELF interpreter in place (offset-guarded, no file growth so bun's appended payload stays intact) to a per-user loader path, and drops a wrapper on PATH that sets LD_LIBRARY_PATH. glibc/arm64/Alpine installs are unchanged. Adds an install-time smoke test that surfaces missing-AVX2 CPUs early.
1 parent 0afe612 commit ef9131e

6 files changed

Lines changed: 99 additions & 9 deletions

File tree

packages/opencode/script/build.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,25 @@ if (fs.existsSync(allSkillsSrc)) {
316316
console.log(`Bundled ${skillEntries.length} skills into all platform dists`)
317317
}
318318

319+
// Bundle the musl runtime (loader + libstdc++/libgcc_s) into x64 musl dists so
320+
// the archive can self-install on old-glibc Linux (e.g. CentOS/RHEL 7) with no
321+
// root and no network: setup.sh points the ELF interpreter at a per-user copy
322+
// of the loader and serves the rest via LD_LIBRARY_PATH. Only x64 musl targets
323+
// carry these libs; glibc/arm64/Alpine installs are untouched.
324+
const muslRuntimeSrc = path.join(__dirname, "musl-runtime", "x64")
325+
if (fs.existsSync(muslRuntimeSrc)) {
326+
for (const key of Object.keys(binaries)) {
327+
const target = findTarget(key)
328+
if (!target || target.os !== "linux" || target.arch !== "x64" || target.abi !== "musl") continue
329+
const destLib = path.join("dist", key, "bin", "lib")
330+
fs.mkdirSync(destLib, { recursive: true })
331+
for (const f of fs.readdirSync(muslRuntimeSrc)) {
332+
fs.copyFileSync(path.join(muslRuntimeSrc, f), path.join(destLib, f))
333+
}
334+
console.log(`Bundled musl runtime into ${key}/bin/lib`)
335+
}
336+
}
337+
319338
if (Script.archive) {
320339
const setupSh = path.join(dir, "..", "..", "scripts", "setup.sh")
321340
const keys = Object.keys(binaries)
655 KB
Binary file not shown.
655 KB
Binary file not shown.
170 KB
Binary file not shown.
2.67 MB
Binary file not shown.

scripts/setup.sh

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,87 @@ if [ ! -f "$SOURCE_BINARY" ]; then
2121
fi
2222

2323
mkdir -p "$INSTALL_DIR" "$METADATA_DIR"
24-
cp "$SOURCE_BINARY" "$TARGET_BINARY"
25-
chmod +x "$TARGET_BINARY"
2624

27-
case "$(uname -s)" in
28-
Darwin)
29-
xattr -dr com.apple.quarantine "$TARGET_BINARY" 2>/dev/null || true
30-
codesign --force --sign - "$TARGET_BINARY" 2>/dev/null || true
31-
codesign -v --verbose=4 "$TARGET_BINARY" 2>/dev/null || true
32-
;;
33-
esac
25+
# Portable musl install: when this archive carries the musl runtime (bin/lib/)
26+
# AND the system lacks the musl loader (old-glibc distros like CentOS/RHEL 7),
27+
# install the real binary under libexec, point its ELF interpreter at a per-user
28+
# loader via an in-place edit (no file growth -> bun's appended payload stays
29+
# intact), and put a small wrapper on PATH. On Alpine the loader already exists,
30+
# so we fall through to the plain copy below.
31+
MUSL_LOADER_SRC="${SCRIPT_DIR}/lib/ld-musl-x86_64.so.1"
32+
if [ -f "$MUSL_LOADER_SRC" ] && [ ! -e "/lib/ld-musl-x86_64.so.1" ]; then
33+
LIBEXEC="$(dirname "$INSTALL_DIR")/libexec/cz-cli"
34+
mkdir -p "$LIBEXEC"
35+
cp -f "$SOURCE_BINARY" "$LIBEXEC/cz-cli.real"
36+
chmod +x "$LIBEXEC/cz-cli.real"
37+
rm -rf "$LIBEXEC/lib"; cp -rf "${SCRIPT_DIR}/lib" "$LIBEXEC/lib"
38+
39+
OLD="/lib/ld-musl-x86_64.so.1" # original ELF interpreter, 24 chars
40+
INTERP=""
41+
for p in "$HOME/.ld.so" "$HOME/.cz-ld.so" "/tmp/ld-musl-x86_64.so.1"; do
42+
[ "${#p}" -le 24 ] || continue
43+
d=$(dirname "$p"); [ -d "$d" ] && [ -w "$d" ] || continue
44+
INTERP="$p"; break
45+
done
46+
[ -n "$INTERP" ] || { print_error "no writable loader path <=24 chars (\$HOME too long?)"; exit 1; }
47+
echo "ELF interpreter -> $INTERP (${#INTERP} chars)"
48+
49+
# In-place patch .interp. PT_INTERP lives at a build-fixed offset (568) in the
50+
# ELF header region; we do NOT scan the whole ~140MB binary. Verify the slot by
51+
# reading it back; only on mismatch scan the first 64 KiB (headers live at the
52+
# start). Every write is guarded by a read-back so a wrong offset aborts rather
53+
# than corrupts.
54+
BIN="$LIBEXEC/cz-cli.real"
55+
read_slot() { dd if="$BIN" bs=1 skip="$1" count=24 2>/dev/null; }
56+
OFF=""
57+
if [ "$(read_slot 568)" = "$OLD" ]; then
58+
OFF=568
59+
else
60+
HEAD=$(mktemp)
61+
dd if="$BIN" bs=4096 count=16 of="$HEAD" 2>/dev/null # first 64 KiB only
62+
M=$(grep -aboF -- "$OLD" "$HEAD" 2>/dev/null || true); OFF=${M%%:*}
63+
rm -f "$HEAD"
64+
fi
65+
case "$OFF" in
66+
''|*[!0-9]*) print_error "musl interpreter string not found in binary"; exit 1 ;;
67+
esac
68+
[ "$(read_slot "$OFF")" = "$OLD" ] || { print_error "unexpected bytes at interp offset $OFF"; exit 1; }
69+
dd if=/dev/zero of="$BIN" bs=1 seek="$OFF" count=24 conv=notrunc 2>/dev/null
70+
printf '%s' "$INTERP" | dd of="$BIN" bs=1 seek="$OFF" conv=notrunc 2>/dev/null
71+
[ "$(read_slot "$OFF" | tr -d '\0')" = "$INTERP" ] || { print_error "interp patch verification failed"; exit 1; }
72+
73+
# Wrapper on PATH: ensure the loader is present, set the lib path, exec the real
74+
# binary. This becomes $TARGET_BINARY so cz-agent and callers work unchanged.
75+
cat > "$TARGET_BINARY" <<EOF
76+
#!/bin/sh
77+
LIBEXEC="$LIBEXEC"
78+
INTERP="$INTERP"
79+
[ -f "\$INTERP" ] || cp -f "\$LIBEXEC/lib/ld-musl-x86_64.so.1" "\$INTERP"
80+
export LD_LIBRARY_PATH="\$LIBEXEC/lib\${LD_LIBRARY_PATH:+:\$LD_LIBRARY_PATH}"
81+
exec "\$LIBEXEC/cz-cli.real" "\$@"
82+
EOF
83+
chmod +x "$TARGET_BINARY"
84+
85+
# Seed the loader now so the first run is clean, then smoke-test. A failure here
86+
# is almost always a missing-AVX2 CPU (SIGILL) — surface it at install time.
87+
[ -f "$INTERP" ] || cp -f "$LIBEXEC/lib/ld-musl-x86_64.so.1" "$INTERP"
88+
if ! "$TARGET_BINARY" --version >/dev/null 2>&1; then
89+
print_error "installed binary failed to run (musl portable)."
90+
print_error "If this CPU lacks AVX2 (grep -c avx2 /proc/cpuinfo == 0) a baseline build is required."
91+
exit 1
92+
fi
93+
else
94+
cp "$SOURCE_BINARY" "$TARGET_BINARY"
95+
chmod +x "$TARGET_BINARY"
96+
97+
case "$(uname -s)" in
98+
Darwin)
99+
xattr -dr com.apple.quarantine "$TARGET_BINARY" 2>/dev/null || true
100+
codesign --force --sign - "$TARGET_BINARY" 2>/dev/null || true
101+
codesign -v --verbose=4 "$TARGET_BINARY" 2>/dev/null || true
102+
;;
103+
esac
104+
fi
34105

35106
# cz-agent: convenience wrapper for `cz-cli agent` (same dir, already on PATH;
36107
# works in any shell and in scripts, unlike a shell alias).

0 commit comments

Comments
 (0)