#!/usr/bin/env bash set -Eeuo pipefail ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) OUT=${1:-$ROOT/dist/csr-intake-x86_64} GUI="$ROOT/gui/target/release/intake-gui" SCANNER="$ROOT/scanner/target/release/pc-intake" MANIFEST="$ROOT/build/release.json" BUNDLE_CONFIG=${CSR_BUNDLE_CONFIG_DIR:-$ROOT/.bundle-secrets} for file in "$GUI" "$SCANNER" "$MANIFEST"; do [[ -f $file ]] || { echo 'Release files are missing. Run bash scripts/build-release.sh first.' >&2 exit 2 } done BUILD_ID=$(python3 "$ROOT/scripts/release-manifest.py" --source-id) GUI_VERSION=$($GUI --version) grep -qF "$BUILD_ID" <<< "$GUI_VERSION" || { echo 'Release binaries are stale. Run bash scripts/build-release.sh first.' >&2 exit 2 } VERSION=$(sed -E 's/^CS-R Intake ([^ ]+).*/\1/' <<< "$GUI_VERSION") TMP=$(mktemp -d) cleanup() { find "$TMP" -depth -delete 2>/dev/null || true; } trap cleanup EXIT APP="$TMP/app" mkdir -p "$APP/bin" "$APP/etc" "$APP/lib" "$APP/python/bin" "$APP/python/lib/python/lib-dynload" "$(dirname "$OUT")" install -m755 "$GUI" "$APP/intake-gui" install -m755 "$SCANNER" "$APP/pc-intake" install -m644 "$MANIFEST" "$APP/release.json" python3 "$ROOT/scripts/release-manifest.py" --verify-release "$APP" PYTHON=$(readlink -f "$(command -v python3)") PYTHON_STDLIB=$(python3 -c 'import sysconfig; print(sysconfig.get_path("stdlib"))') install -m755 "$PYTHON" "$APP/python/bin/python3" cp -a "$PYTHON_STDLIB/lib-dynload/." "$APP/python/lib/python/lib-dynload/" for tool in nmcli id timeout curl sh; do source=$(command -v "$tool" 2>/dev/null || true) [[ -n $source ]] || { echo "Required portable runtime tool is missing: $tool" >&2; exit 2; } install -m755 "$(readlink -f "$source")" "$APP/bin/$tool" done for tool in smartctl stress-ng ffmpeg ffplay arecord aplay speaker-test lsusb xdg-open mpv lspci glmark2 memtester; do source=$(command -v "$tool" 2>/dev/null || true) [[ -z $source ]] || install -m755 "$(readlink -f "$source")" "$APP/bin/$tool" done for certificates in /etc/ssl/certs/ca-certificates.crt /etc/ca-certificates/extracted/tls-ca-bundle.pem; do if [[ -f $certificates ]]; then install -m644 "$certificates" "$APP/etc/ca-certificates.crt"; break; fi done python3 - "$PYTHON_STDLIB" "$APP/python/lib/python.zip" <<'PY' from pathlib import Path import sys, zipfile source = Path(sys.argv[1]) output = Path(sys.argv[2]) excluded = {'site-packages', '__pycache__', 'test', 'tests', 'idlelib', 'tkinter', 'turtledemo', 'ensurepip', 'venv'} with zipfile.ZipFile(output, 'w', zipfile.ZIP_DEFLATED, compresslevel=9) as archive: for path in sorted(source.rglob('*.py')): relative = path.relative_to(source) if any(part in excluded for part in relative.parts): continue archive.write(path, relative) PY python3 - "$APP" <<'PY' from pathlib import Path import re, shutil, subprocess, sys app = Path(sys.argv[1]) lib = app / 'lib' seeds = [app/'intake-gui', app/'pc-intake', app/'python/bin/python3', *sorted((app/'bin').iterdir()), *sorted((app/'python/lib/python/lib-dynload').glob('*.so'))] optional = ['libX11.so.6','libXcursor.so.1','libXi.so.6','libXrandr.so.2','libxcb.so.1','libXau.so.6','libXdmcp.so.6','libwayland-client.so.0','libxkbcommon.so.0','libEGL.so.1','libGL.so.1','libGLX.so.0','libOpenGL.so.0','libnss_files.so.2','libnss_dns.so.2','libresolv.so.2'] cache = subprocess.run(['ldconfig','-p'], text=True, check=True, capture_output=True).stdout for name in optional: match = re.search(r'^\s*'+re.escape(name)+r'.* => (\S+)$', cache, re.M) if match: seeds.append(Path(match.group(1))) queue, seen = list(seeds), set() while queue: binary = queue.pop() try: key = binary.resolve() except OSError: continue if key in seen or not key.is_file(): continue seen.add(key) result = subprocess.run(['ldd', str(key)], text=True, capture_output=True) for line in result.stdout.splitlines(): paths = re.findall(r'(/[^\s()]+)', line) for value in paths: dependency = Path(value) if dependency.exists(): queue.append(dependency) for source in sorted(seen): if source.is_relative_to(app.resolve()): continue destination = lib / source.name if not destination.exists(): shutil.copy2(source, destination) loader = Path('/usr/lib/ld-linux-x86-64.so.2') if not loader.exists(): loader = Path('/lib64/ld-linux-x86-64.so.2').resolve() shutil.copy2(loader, lib/'ld-linux-x86-64.so.2') PY contains_credentials=0 if [[ -f $BUNDLE_CONFIG/inventory-api-key && -f $BUNDLE_CONFIG/inventory-server.crt ]]; then key=$(tr -d '\r\n' < "$BUNDLE_CONFIG/inventory-api-key") [[ -n $key && $key != *[[:space:]]* ]] || { echo 'Bundled API key is malformed.' >&2; exit 2; } openssl x509 -in "$BUNDLE_CONFIG/inventory-server.crt" -noout -checkend 0 >/dev/null || { echo 'Bundled inventory certificate is invalid or expired.' >&2; exit 2; } mkdir -m700 "$APP/default-config" install -m600 "$BUNDLE_CONFIG/inventory-api-key" "$APP/default-config/inventory-api-key" install -m600 "$BUNDLE_CONFIG/inventory-server.crt" "$APP/default-config/inventory-server.crt" printf '%s\n' '{"url":"https://192.168.178.17"}' > "$APP/default-config/inventory.json" chmod 600 "$APP/default-config/inventory.json" contains_credentials=1 elif [[ -e $BUNDLE_CONFIG/inventory-api-key || -e $BUNDLE_CONFIG/inventory-server.crt ]]; then echo 'Both inventory-api-key and inventory-server.crt are required.' >&2 exit 2 fi env -i \ PYTHONHOME="$APP/python" \ PYTHONPATH="$APP/python/lib/python.zip:$APP/python/lib/python/lib-dynload" \ "$APP/lib/ld-linux-x86-64.so.2" --library-path "$APP/lib" "$APP/python/bin/python3" \ -c 'import argparse, datetime, hashlib, json, pathlib, sqlite3, ssl, urllib.request, uuid; print("Bundled Python OK")' python3 - "$APP" "$TMP/payload.gz" "$TMP/payload.manifest" <<'PY' from pathlib import Path import gzip, os, struct, sys root, output, manifest = map(Path, sys.argv[1:]) files = sorted(path for path in root.rglob('*') if path.is_file()) lines=[] with output.open('wb') as raw, gzip.GzipFile(fileobj=raw, mode='wb', compresslevel=9, mtime=0) as archive: archive.write(b'CSRPKG3\0') for path in files: name = path.relative_to(root).as_posix().encode() data = path.read_bytes() mode = path.stat().st_mode & 0o777 archive.write(struct.pack('&2; exit 2; } if ldd "$OUT" 2>&1 | grep -qv 'statically linked\|not a dynamic executable'; then echo 'Launcher unexpectedly depends on host libraries.' >&2; exit 2 fi "$OUT" --check-bundle echo "Standalone ELF created: $OUT"