#!/usr/bin/env bash # docker-prep — pack @bytelyst/* tarballs for hermetic Docker builds. # # CANONICAL TEMPLATE — do not hand-edit copies in product repos. # Sync via: bash learning_ai_common_plat/scripts/sync-docker-prep.sh # # Usage: # bash scripts/docker-prep.sh # pack + rewrite (default) # bash scripts/docker-prep.sh --restore # undo rewrite, drop .bak/tarballs # bash scripts/docker-prep.sh --dry-run # list packs/rewrites, no side effects # bash scripts/docker-prep.sh --check # exit 1 if rewrites still in place # bash scripts/docker-prep.sh --strip-overrides # remove pnpm.overrides only # bash scripts/docker-prep.sh --force # bypass idempotency guard # bash scripts/docker-prep.sh --keep # do NOT auto-restore on error # # Behavior: # - Auto-discovers package.json files containing @bytelyst/* deps (max-depth 3). # - Idempotent: refuses to run pack mode if any *.bak exists (use --force). # - Safe: on error, auto-restores .bak files unless --keep is passed. # - Portable: sed -i '' for macOS, sed -i for Linux. # # Phase B refs: B1, B2, B5, B6, B7, B8 of docker-build-optimization-roadmap.md set -uo pipefail # ── CLI parsing ─────────────────────────────────────────────────── MODE=pack FORCE=false KEEP_ON_ERROR=false DRY_RUN=false while [ $# -gt 0 ]; do case "$1" in --restore) MODE=restore ;; --check) MODE=check ;; --strip-overrides) MODE=strip-overrides ;; --dry-run) DRY_RUN=true ;; --force) FORCE=true ;; --keep) KEEP_ON_ERROR=true ;; -h|--help) sed -n '2,21p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; *) echo "✗ Unknown arg: $1" >&2; exit 2 ;; esac shift done SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" COMMON_PLAT="${COMMON_PLAT_DIR:-${REPO_DIR}/../learning_ai_common_plat}" TARBALL_DIR="${REPO_DIR}/.docker-deps" # Portable sed -i (BSD vs GNU) sed_inplace() { if [[ "$(uname)" == "Darwin" ]]; then sed -i '' "$@" else sed -i "$@" fi } # ── Discover package.json files with @bytelyst/* refs ──────────── discover_pkg_files() { find "$REPO_DIR" -maxdepth 3 -name 'package.json' \ -not -path '*/node_modules/*' \ -not -path '*/.docker-deps/*' \ -not -path '*/dist/*' \ -not -path '*/.next/*' 2>/dev/null \ | while read -r f; do if grep -q '"@bytelyst/' "$f" 2>/dev/null; then echo "$f" fi done } # ── relative prefix from a package.json's dir back to repo root ── rel_prefix_for() { local pkg_file="$1" local pkg_dir pkg_dir="$(dirname "$pkg_file")" # If package.json is at repo root, prefix is ./ if [[ "$pkg_dir" == "$REPO_DIR" ]]; then echo "" return fi # Count dir depth from repo root → produce ../ * depth local rel="${pkg_dir#$REPO_DIR/}" local depth depth=$(echo "$rel" | awk -F/ '{print NF}') local prefix="" for ((i=0; i/dev/null 2>&1; then echo "✗ Tarballs still in $TARBALL_DIR" found=1 fi for bak in $(find "$REPO_DIR" -name 'package.json.bak' -not -path '*/node_modules/*' 2>/dev/null); do echo "✗ Backup still present: $bak" found=1 done if [ $found -eq 0 ]; then echo "✓ docker-prep state clean" exit 0 fi exit 1 fi # ── --restore mode (idempotent) ────────────────────────────────── restore_all() { local restored=0 for bak in $(find "$REPO_DIR" -name 'package.json.bak' -not -path '*/node_modules/*' 2>/dev/null); do if $DRY_RUN; then echo " [dry-run] would restore ${bak%.bak}" else mv "$bak" "${bak%.bak}" echo " ✓ Restored ${bak%.bak}" fi restored=$((restored+1)) done if [ -d "$TARBALL_DIR" ]; then if $DRY_RUN; then echo " [dry-run] would clear tarballs from $TARBALL_DIR (preserving .gitkeep)" else # Preserve .gitkeep so the dir survives for fresh clones / Dockerfile COPY find "$TARBALL_DIR" -mindepth 1 ! -name '.gitkeep' -delete 2>/dev/null || true echo " ✓ Cleared tarballs from $TARBALL_DIR (preserved .gitkeep)" fi fi if [ $restored -eq 0 ] && [ ! -d "$TARBALL_DIR" ]; then echo " (nothing to restore — already clean)" fi } if [[ "$MODE" == "restore" ]]; then echo "=== docker-prep: restoring original state ===" restore_all echo "Done." exit 0 fi # ── --strip-overrides (B8) ────────────────────────────────────── if [[ "$MODE" == "strip-overrides" ]]; then echo "=== docker-prep: stripping pnpm.overrides ===" for pkg in $(discover_pkg_files); do if grep -q '"overrides"' "$pkg"; then if $DRY_RUN; then echo " [dry-run] would strip overrides from $pkg" else PKG_FILE_ARG="$pkg" node "$SCRIPT_DIR/_docker-prep-strip.js" echo " ✓ Stripped @bytelyst/* overrides from $pkg" fi fi done echo "Done." exit 0 fi # ── Pack mode (default) ───────────────────────────────────────── echo "=== docker-prep: packing @bytelyst/* tarballs ===" # B2: idempotency guard EXISTING_BAKS=$(find "$REPO_DIR" -name 'package.json.bak' -not -path '*/node_modules/*' 2>/dev/null | head -5) if [[ -n "$EXISTING_BAKS" ]] && ! $FORCE; then echo "✗ Existing *.bak files detected — previous docker-prep run not cleaned up:" echo "$EXISTING_BAKS" | sed 's/^/ /' echo "" echo "Run one of:" echo " bash scripts/docker-prep.sh --restore # clean up" echo " bash scripts/docker-prep.sh --force # ignore guard (DANGEROUS)" exit 1 fi # Verify common-plat sibling if [ ! -d "$COMMON_PLAT/packages" ]; then echo "✗ Cannot find common-plat packages at $COMMON_PLAT/packages" echo " Clone learning_ai_common_plat as a sibling directory, or set COMMON_PLAT_DIR." exit 1 fi # B5: trap to auto-restore on error cleanup_on_error() { local code=$? if [ $code -ne 0 ] && ! $KEEP_ON_ERROR && ! $DRY_RUN; then echo "" echo "⚠ docker-prep failed (exit $code) — auto-restoring (pass --keep to disable)..." restore_all fi return $code } trap cleanup_on_error EXIT if $DRY_RUN; then echo " [dry-run] would build @bytelyst/* packages in $COMMON_PLAT" else echo "Building @bytelyst/* packages..." (cd "$COMMON_PLAT" && pnpm -r --filter './packages/*' build) >/dev/null fi # Pack each package if $DRY_RUN; then echo " [dry-run] would pack packages to $TARBALL_DIR" else mkdir -p "$TARBALL_DIR" # Clear stale tarballs but preserve .gitkeep find "$TARBALL_DIR" -mindepth 1 ! -name '.gitkeep' -delete 2>/dev/null || true fi TARBALL_MAP_FILE=$(mktemp) # Note: not adding to existing EXIT trap; clean up explicitly at end for pkg_dir in "$COMMON_PLAT"/packages/*/; do pkg_name=$(node -p "require('${pkg_dir}package.json').name" 2>/dev/null || true) [[ -z "$pkg_name" ]] && continue if $DRY_RUN; then echo " [dry-run] would pack $pkg_name" continue fi echo " Packing $pkg_name..." tarball=$(cd "$pkg_dir" && pnpm pack --pack-destination "$TARBALL_DIR" 2>/dev/null | tail -1) filename=$(basename "$tarball") echo "${pkg_name}=${filename}" >> "$TARBALL_MAP_FILE" echo " -> $filename" done if $DRY_RUN; then echo "" echo " [dry-run] would rewrite refs in:" discover_pkg_files | sed 's/^/ /' rm -f "$TARBALL_MAP_FILE" trap - EXIT exit 0 fi # ── Rewrite + inject overrides ─────────────────────────────────── echo "" echo "Rewriting package.json @bytelyst/* refs to .docker-deps/ tarballs..." PKG_FILES=$(discover_pkg_files) if [[ -z "$PKG_FILES" ]]; then echo "✗ No package.json with @bytelyst/* deps found in $REPO_DIR" rm -f "$TARBALL_MAP_FILE" trap - EXIT exit 1 fi for pkg_file in $PKG_FILES; do prefix="$(rel_prefix_for "$pkg_file")" cp "$pkg_file" "${pkg_file}.bak" tmp="${pkg_file}.tmp" cp "$pkg_file" "$tmp" while IFS='=' read -r pkg_name tarball; do [[ -z "$pkg_name" ]] && continue sed_inplace "s|\"${pkg_name}\": \"[^\"]*\"|\"${pkg_name}\": \"file:${prefix}.docker-deps/${tarball}\"|g" "$tmp" done < "$TARBALL_MAP_FILE" mv "$tmp" "$pkg_file" echo " ✓ Rewrote $pkg_file" done # Inject overrides for transitive @bytelyst/* deps echo "" echo "Injecting pnpm.overrides..." for pkg_file in $PKG_FILES; do prefix="$(rel_prefix_for "$pkg_file")" overrides="" while IFS='=' read -r pkg_name tarball; do [[ -z "$pkg_name" ]] && continue if [[ -n "$overrides" ]]; then overrides="$overrides, "; fi overrides="$overrides\"${pkg_name}\": \"file:${prefix}.docker-deps/${tarball}\"" done < "$TARBALL_MAP_FILE" if [[ -n "$overrides" ]]; then PKG_FILE_ARG="$pkg_file" OVERRIDES_ARG="{${overrides}}" \ node "$SCRIPT_DIR/_docker-prep-inject.js" echo " ✓ Injected overrides into $pkg_file" fi done rm -f "$TARBALL_MAP_FILE" trap - EXIT echo "" echo "✅ Done. Tarballs in $TARBALL_DIR" echo "" echo "Next:" echo " docker compose build # build images" echo " bash scripts/docker-prep.sh --restore # undo when done" echo " bash scripts/docker-prep.sh --check # verify clean state"