#!/bin/zsh
#
# build-gcc.sh — Build the C toolchain for Hammerhead
#
# Builds zlib, GMP, MPFR, MPC, binutils, GCC 14.2, flex, and bison from source.
# Called by hh-build as a pre-build toolchain step.
# Mirrors the Tier 1 logic in tools/bootstrap.sh.
#
# Usage: build-gcc.sh [--jobs N]
#   --jobs N    Parallel make jobs (default: 4)
#
# Environment (set by hh-build before calling):
#   SRC         — path to usr/src
#   CONTRIB     — path to usr/src/contrib ($SRC/contrib)
#
# Source tarballs must be pre-extracted into:
#   $CONTRIB/zlib/      — zlib 1.3.1
#   $CONTRIB/gmp/       — GMP 6.3.0
#   $CONTRIB/mpfr/      — MPFR 4.2.1
#   $CONTRIB/mpc/       — MPC 1.3.1
#   $CONTRIB/binutils/  — binutils 2.43.1
#   $CONTRIB/gcc/       — GCC 14.2.0
#   $CONTRIB/flex/      — flex 2.6.4
#   $CONTRIB/bison/     — bison 3.8.2
#
# Installs to staging root: $SRC/tools/.gcc-toolchain/usr
# hh-build sets GNUC_ROOT to the staging root for subsequent build steps.
#

set -euo pipefail
# zsh: empty globs expand to nothing (default would error out the script).
# Some paths use *.la / libstdc++.so.6.0.* / include-fixed/* patterns that
# may have zero matches under partial-build conditions.
setopt NULL_GLOB

# Verify /dev/null is functional. Restore devfs symlink if corrupted.
if [[ ! -c /dev/null ]]; then
    echo "WARNING: /dev/null corrupted — restoring devfs symlink" >&2
    rm -f /dev/null
    ln -s ../devices/pseudo/mm@0:null /dev/null
fi

JOBS=4

while [[ $# -gt 0 ]]; do
    case "$1" in
        --jobs)  JOBS="$2"; shift 2 ;;
        -j*)     JOBS="${1#-j}"; shift ;;
        *)       echo "Unknown argument: $1" >&2; exit 1 ;;
    esac
done

# ── Paths ────────────────────────────────────────────────────────────────

: "${SRC:?SRC must be set}"
: "${CONTRIB:=$SRC/contrib}"

STAGE="$SRC/tools/.gcc-toolchain"

ZLIB_SRC="$CONTRIB/zlib"
GMP_SRC="$CONTRIB/gmp"
MPFR_SRC="$CONTRIB/mpfr"
MPC_SRC="$CONTRIB/mpc"
BINUTILS_SRC="$CONTRIB/binutils"
GCC_SRC="$CONTRIB/gcc"
FLEX_SRC="$CONTRIB/flex"
BISON_SRC="$CONTRIB/bison"

# ── Logging ──────────────────────────────────────────────────────────────

log()  { echo "==> [build-gcc] $*"; }
log2() { echo "  -> $*"; }
die()  { echo "FATAL: $*" >&2; exit 1; }

# ── No stamp caching ─────────────────────────────────────────────────────
# Stamp-based caching removed: it masked bugs (GCC libstdc++ race,
# missing cmake platform files) by silently skipping rebuilds.
# Full rebuilds every time are slower but reliable.
log "Full C toolchain rebuild (no stamp caching)"

# ── Verify Sources ──────────────────────────────────────────────────────

[[ -d "$GCC_SRC" ]]      || die "GCC source not found at $GCC_SRC"
[[ -d "$BINUTILS_SRC" ]]  || die "Binutils source not found at $BINUTILS_SRC"

# ── Environment ──────────────────────────────────────────────────────────

# Use host compiler for Stage 1 build
HOST_OS="$(uname -s)"
if [[ "$HOST_OS" == "Hammerhead" ]]; then
    export CC="/usr/bin/gcc -m64"
    export CXX="/usr/bin/g++ -m64"
else
    export CC="/usr/gcc/14/bin/gcc -m64"
    export CXX="/usr/gcc/14/bin/g++ -m64"
fi

# Hammerhead's uname -s returns "Hammerhead" which config.guess doesn't
# recognize. Pass explicit build/host triplet to all configure scripts.
# zsh array — expands to two separate arguments properly
BUILD_TRIPLET=(--build=x86_64-pc-solaris2.11 --host=x86_64-pc-solaris2.11)

export CFLAGS="-m64 -O2"
export CXXFLAGS="-m64 -O2"
export LDFLAGS="-m64 -L${STAGE}/usr/lib"
export CPPFLAGS="-I${STAGE}/usr/include"
export PATH="${STAGE}/usr/bin:$PATH"

export PKG_CONFIG_PATH="${STAGE}/usr/lib/pkgconfig"
export LD_LIBRARY_PATH="${STAGE}/usr/lib"
export LIBRARY_PATH="${STAGE}/usr/lib"

# Note: system libtool (/usr/bin/libtool) may be a "configured" wrapper
# from bootstrap-hh.sh that breaks GCC's libstdc++ build.  The workaround
# is applied inside build_gcc() only — GMP and other components need a
# working libtool.

# makeinfo/texinfo not available on Hammerhead — export so all recursive
# make invocations inherit it (passing on command line alone isn't enough
# for binutils/GCC's deeply nested recursive builds)
# makeinfo/autoconf/automake/aclocal not available on Hammerhead — export
# so all recursive make invocations inherit them
export MAKEINFO=true
export AUTOCONF=true
export ACLOCAL=true
export AUTOMAKE=true
export AUTOHEADER=true

# ── Helper ───────────────────────────────────────────────────────────────

clean_la_files() {
    find "$STAGE" -name '*.la' -delete 2>/dev/null || true
}

# Touch autotools-generated files so Make doesn't try to regenerate
# them (aclocal/automake/autoconf not available on Hammerhead).
# Must be called before configure for each autotools-based component.
prevent_autoreconf() {
    local srcdir="$1"
    find "$srcdir" \( -name 'configure' -o -name 'Makefile.in' \
        -o -name 'aclocal.m4' -o -name 'config.h.in' \) \
        -exec touch {} + 2>/dev/null || true
}

# ── Build zlib ───────────────────────────────────────────────────────────

build_zlib() {
    # On self-hosted Hammerhead, zlib is already part of the base system
    # (built by hh-build from contrib/zlib with illumos Makefiles).
    # Only build standalone zlib if a configure script exists (OI bootstrap).
    [[ -d "$ZLIB_SRC" ]] || { log2 "zlib source not found, skipping"; return; }
    [[ -f "$ZLIB_SRC/configure" ]] || { log2 "zlib: no configure (using system zlib)"; return; }
    log "Building zlib 1.3.1"
    cd "$ZLIB_SRC"
    ./configure --prefix=/usr --libdir=/usr/lib
    gmake -j"$JOBS"
    gmake install DESTDIR="$STAGE"
    clean_la_files
}

# ── Build GMP ────────────────────────────────────────────────────────────

build_gmp() {
    [[ -d "$GMP_SRC" ]] || { log2 "GMP source not found, skipping"; return; }
    log "Building GMP 6.3.0"
    cd "$GMP_SRC"
    gmake distclean 2>/dev/null || true
    prevent_autoreconf "$GMP_SRC"
    ./configure $BUILD_TRIPLET \
        --prefix=/usr \
        --libdir=/usr/lib \
        --disable-cxx \
        --enable-fat
    gmake -j"$JOBS"
    gmake install DESTDIR="$STAGE"
    clean_la_files
}

# ── Build MPFR ───────────────────────────────────────────────────────────

build_mpfr() {
    [[ -d "$MPFR_SRC" ]] || { log2 "MPFR source not found, skipping"; return; }
    log "Building MPFR 4.2.1"
    cd "$MPFR_SRC"
    gmake distclean 2>/dev/null || true
    prevent_autoreconf "$MPFR_SRC"
    ./configure $BUILD_TRIPLET \
        --prefix=/usr \
        --libdir=/usr/lib \
        --with-gmp="$STAGE/usr"
    gmake -j"$JOBS"
    gmake install DESTDIR="$STAGE"
    clean_la_files
}

# ── Build MPC ────────────────────────────────────────────────────────────

build_mpc() {
    [[ -d "$MPC_SRC" ]] || { log2 "MPC source not found, skipping"; return; }
    log "Building MPC 1.3.1"
    cd "$MPC_SRC"
    gmake distclean 2>/dev/null || true
    prevent_autoreconf "$MPC_SRC"
    ./configure $BUILD_TRIPLET \
        --prefix=/usr \
        --libdir=/usr/lib \
        --with-gmp="$STAGE/usr" \
        --with-mpfr="$STAGE/usr"
    gmake -j"$JOBS"
    gmake install DESTDIR="$STAGE"
    clean_la_files
}

# ── Build binutils ───────────────────────────────────────────────────────

build_binutils() {
    [[ -d "$BINUTILS_SRC" ]] || die "Binutils source not found at $BINUTILS_SRC"
    log "Building binutils 2.43.1"
    prevent_autoreconf "$BINUTILS_SRC"
    rm -rf "$BINUTILS_SRC/build"
    mkdir -p "$BINUTILS_SRC/build"
    cd "$BINUTILS_SRC/build"
    ../configure $BUILD_TRIPLET \
        --prefix=/usr \
        --libdir=/usr/lib \
        --enable-64-bit-bfd \
        --enable-plugins \
        --disable-werror \
        --disable-nls \
        --without-zstd \
        --with-system-zlib \
        --with-zlib="$STAGE/usr" \
        --target=x86_64-pc-solaris2.11 \
        MAKEINFO=true
    # binutils bfd has a parallel build race (ar: archive.o: No such file)
    # Use -j1 for the link phase; compile phase is fine parallel
    gmake -j"$JOBS" MAKEINFO=true || gmake -j1 MAKEINFO=true
    gmake install DESTDIR="$STAGE" MAKEINFO=true
}

# ── Build GCC ────────────────────────────────────────────────────────────

build_gcc() {
    [[ -d "$GCC_SRC" ]] || die "GCC source not found at $GCC_SRC"
    log "Building GCC 14.2.0"
    prevent_autoreconf "$GCC_SRC"
    # Fresh out-of-tree build — stale libtool/config from partial builds
    # causes "unrecognized option" failures in libstdc++
    rm -rf "$GCC_SRC/build"
    mkdir -p "$GCC_SRC/build"
    cd "$GCC_SRC/build"

    # Hide bootstrap-hh.sh's "configured" libtool during GCC's configure.
    # GCC's configure uses /usr/share/aclocal/libtool.m4 to generate a
    # libtool script.  The bootstrap libtool.m4 generates a broken one
    # that rejects -I flags, killing libstdc++.  Temporarily rename both
    # the binary and the m4 macros so GCC falls back to its bundled
    # ltmain.sh with generic defaults.
    #
    # IMPORTANT: Always restore first in case a previous run failed before
    # restoring.  Then hide if needed.  Use a trap to guarantee restoration
    # even if this build fails.
    local libtool_hidden=false
    local _lt_files="/usr/bin/libtool /usr/bin/libtoolize /usr/share/aclocal/libtool.m4 /usr/share/aclocal/ltdl.m4"

    # Phase 1: Restore any leftover .gcc-hide files from a previous failed run
    for f in $_lt_files; do
        [[ -f "${f}.gcc-hide" && ! -f "$f" ]] && mv "${f}.gcc-hide" "$f" 2>/dev/null || true
    done

    # Phase 2: Replace system libtool.m4 with GCC's bundled version.
    # The bootstrap libtool.m4 (2021-2022) generates libtool scripts that
    # reject -I flags in CXX compile mode.  GCC's bundled libtool.m4 (2009)
    # works correctly.  GCC sub-projects (libstdc++, etc.) run their own
    # configure which finds /usr/share/aclocal/libtool.m4 via aclocal's
    # search path — we need the GCC-compatible version there.
    if [[ -f /usr/bin/libtool ]]; then
        log2 "Replacing system libtool.m4 with GCC-bundled version for build"
        for f in $_lt_files; do
            [[ -f "$f" ]] && mv "$f" "${f}.gcc-hide" 2>/dev/null || true
        done
        # Put GCC's bundled libtool.m4 where aclocal can find it
        if [[ -f "$GCC_SRC/libtool.m4" ]]; then
            cp "$GCC_SRC/libtool.m4" /usr/share/aclocal/libtool.m4
        fi
        libtool_hidden=true
    fi

    # Phase 3: Trap to guarantee restoration on ANY exit (success or failure)
    restore_libtool() {
        for f in $_lt_files; do
            [[ -f "${f}.gcc-hide" ]] && mv "${f}.gcc-hide" "$f" 2>/dev/null || true
        done
        # Remove the GCC-bundled copy we placed
        rm -f /usr/share/aclocal/libtool.m4.gcc-bundled 2>/dev/null || true
    }
    trap restore_libtool EXIT

    # Hammerhead: force CC_FOR_TARGET/CXX_FOR_TARGET empty so configure
    # falls back to the in-tree xgcc/xg++ branch in GCC_TARGET_TOOL
    # (gcc/configure.ac). If we leave these unset, NCN_STRICT_CHECK_TARGET_TOOLS
    # (gcc/config/acx.m4) adopts host CXX into @CXX_FOR_TARGET@, which then
    # propagates as empty through EXTRA_TARGET_FLAGS to libstdc++-v3's
    # recursive make, breaking libsupc++/libtool's -I argument.
    # We keep host CC/CXX set so configure's own C compiler detection works.
    # --with-as/--with-ld use deployed system paths (not $STAGE) because
    # GCC bakes these into the binary.
    #
    # --with-local-prefix=/opt/hh-gcc/no-local-include: point GCC's
    # LOCAL_INCLUDE_DIR at a nonexistent path so /usr/local/include is
    # NOT auto-searched. This protects Hammerhead builds from coral
    # packages installed at /usr/local — without this, GCC searches
    # /usr/local/include BEFORE /usr/include, and any coral-installed
    # header (e.g., ncurses' /usr/local/include/curses.h) takes
    # precedence over the system headers the build expects. Builds
    # that explicitly want /usr/local can still use `-I/usr/local/include`.
    ../configure $BUILD_TRIPLET \
        CC_FOR_TARGET= \
        CXX_FOR_TARGET= \
        CC_FOR_BUILD="$CC" \
        CXX_FOR_BUILD="$CXX" \
        --prefix=/usr \
        --libdir=/usr/lib \
        --libexecdir=/usr/libexec \
        --with-local-prefix=/opt/hh-gcc/no-local-include \
        --enable-languages=c,c++ \
        --enable-shared \
        --enable-threads=posix \
        --enable-__cxa_atexit \
        --enable-clocale=gnu \
        --enable-gnu-unique-object \
        --enable-linker-build-id \
        --disable-multilib \
        --disable-werror \
        --disable-nls \
        --disable-bootstrap \
        --disable-libstdcxx-pch \
        --disable-lto \
        --without-zstd \
        --with-gmp="$STAGE/usr" \
        --with-mpfr="$STAGE/usr" \
        --with-mpc="$STAGE/usr" \
        --with-as=/usr/bin/as \
        --with-ld=/usr/bin/gnu-ld-wrapper \
        --with-gnu-as \
        --with-gnu-ld \
        --with-system-zlib \
        MAKEINFO=true
    # Build with -k (keep going). libstdc++-v3 has a known GCC 14.2
    # command-line propagation bug where $(CXX) expands to empty in
    # libsupc++'s recursive make, causing libtool to reject -I. The
    # core compiler (xgcc, xg++, libgcc, binutils linkage) builds
    # correctly; only libstdc++ shared library fails. We fall back to
    # the system libstdc++ (copied below from /usr/lib).
    gmake -j"$JOBS" -k MAKEINFO=true || true
    # Verify the core compiler actually built before declaring success.
    if [[ ! -x "$GCC_SRC/build/gcc/xgcc" ]] || [[ ! -x "$GCC_SRC/build/gcc/xg++" ]]; then
        die "GCC core build failed: xgcc/xg++ not produced"
    fi
    gmake install DESTDIR="$STAGE" MAKEINFO=true -k || true

    # libtool's DESTDIR install skips shared libraries on illumos.
    # Manually copy them from the build tree to the staging area.
    # If libstdc++ failed to build, fall back to the system copy at
    # /usr/lib so we still ship a working C++ runtime.
    log2 "Installing shared libraries to staging area"
    local builddir="$GCC_SRC/build"
    # Quote glob patterns so zsh doesn't try to expand them at loop setup.
    # Use (N) qualifier below so no-match globs expand to empty instead
    # of erroring under zsh's default NOMATCH.
    local lib found sys sys_matches
    for lib in 'libstdc++.so.6.0.*' libgcc_s.so.1 libatomic.so.1 \
               libgomp.so.1 libssp.so.0 libitm.so.1 libquadmath.so.0; do
        found=$(find "$builddir" -name "$lib" -not -path '*/\.*' 2>/dev/null | head -1)
        if [[ -n "$found" ]]; then
            cp "$found" "$STAGE/usr/lib/"
            log2 "  Installed $lib (from build tree)"
        else
            # Fall back to system copy via find (avoids zsh glob pitfalls)
            sys_matches=$(find /usr/lib -maxdepth 1 -name "$lib" 2>/dev/null)
            if [[ -n "$sys_matches" ]]; then
                echo "$sys_matches" | while IFS= read -r sys; do
                    cp "$sys" "$STAGE/usr/lib/"
                    log2 "  Installed $(basename "$sys") (from /usr/lib)"
                done
            fi
        fi
    done

    # Restore system libtool — handled by EXIT trap (restore_libtool)
    # but also restore explicitly here for clarity
    restore_libtool
    trap - EXIT

    # GCC's fixincludes captures stale system headers into include-fixed/.
    # Remove opensslconf.h — it's an ancient OpenSSL 1.0.x copy that
    # shadows LibreSSL's opensslfeatures.h, hiding TLS 1.3 support.
    # Use find instead of glob so zsh NOMATCH doesn't abort the script
    # when libstdc++ failure leaves include-fixed/ incomplete.
    find "$STAGE/usr/lib/gcc/x86_64-pc-solaris2.11" \
        -path '*/include-fixed/openssl/opensslconf.h' -delete 2>/dev/null || true
    find "$STAGE/usr/lib/gcc/x86_64-pc-solaris2.11" \
        -path '*/include-fixed/openssl' -type d -empty -delete 2>/dev/null || true

    # GCC installs runtime libs to /usr/lib/amd64/ due to host multilib
    # detection.  Flatten to /usr/lib/ for hammerhead BSD layout.
    if [[ -d "$STAGE/usr/lib/amd64" ]]; then
        log2 "Flattening /usr/lib/amd64 → /usr/lib"
        # Use find so zsh doesn't choke on an empty dir
        find "$STAGE/usr/lib/amd64" -mindepth 1 -maxdepth 1 \
            -exec cp -af {} "$STAGE/usr/lib/" \;
        rm -rf "$STAGE/usr/lib/amd64"
    fi

    # Compiler symlinks
    log2 "Creating compiler symlinks"
    cd "$STAGE/usr/bin"
    ln -sf gcc cc
    ln -sf g++ c++

    cd "$STAGE/usr/lib"
    [[ -f libstdc++.so.6 ]] && ln -sf libstdc++.so.6 libstdc++.so
    [[ -f libgcc_s.so.1 ]]  && ln -sf libgcc_s.so.1 libgcc_s.so
    [[ -f libatomic.so.1 ]] && ln -sf libatomic.so.1 libatomic.so

    # Switch to freshly-built GCC for subsequent steps (flex, bison)
    export CC="$STAGE/usr/bin/gcc -m64"
    export CXX="$STAGE/usr/bin/g++ -m64"
}

# ── Build flex ───────────────────────────────────────────────────────────

build_flex() {
    [[ -d "$FLEX_SRC" ]] || { log2 "flex source not found, skipping"; return; }
    log "Building flex 2.6.4"
    cd "$FLEX_SRC"
    gmake distclean 2>/dev/null || true
    # Touch AFTER distclean so timestamps are correct
    prevent_autoreconf "$FLEX_SRC"
    ./configure $BUILD_TRIPLET \
        --prefix=/usr \
        --libdir=/usr/lib \
        --disable-nls
    gmake -j"$JOBS" -C src
    gmake -j"$JOBS" -C lib || true
    gmake -C src install DESTDIR="$STAGE"
    # Install libfl manually (flex install-recursive tries to install docs)
    mkdir -p "$STAGE/usr/lib" "$STAGE/usr/include"
    cp lib/.libs/libfl.a "$STAGE/usr/lib/" 2>/dev/null || true
    cp src/FlexLexer.h "$STAGE/usr/include/" 2>/dev/null || true
}

# ── Build bison ──────────────────────────────────────────────────────────

build_bison() {
    [[ -d "$BISON_SRC" ]] || { log2 "bison source not found, skipping"; return; }
    log "Building bison 3.8.2"
    cd "$BISON_SRC"
    gmake distclean 2>/dev/null || true
    prevent_autoreconf "$BISON_SRC"
    # Touch pre-generated parser/lexer outputs so Make doesn't regenerate
    # them (bison can't bootstrap itself without a pre-existing bison)
    touch src/parse-gram.c src/parse-gram.h src/scan-*.c 2>/dev/null || true
    ac_cv_libtextstyle=no \
    M4=/usr/bin/m4 \
    ./configure $BUILD_TRIPLET \
        --prefix=/usr \
        --libdir=/usr/lib \
        --disable-nls
    # Build all targets but ignore doc/test failures (bison.info needs
    # makeinfo, which isn't on Hammerhead). Then install manually.
    gmake -j"$JOBS" || true
    # Verify the binary actually built
    [[ -f src/bison ]] || die "bison binary not found after build"
    # Manual install (gmake install tries to install docs and fails)
    mkdir -p "$STAGE/usr/bin" "$STAGE/usr/lib" "$STAGE/usr/share/bison"
    cp src/bison "$STAGE/usr/bin/"
    cp lib/liby.a "$STAGE/usr/lib/" 2>/dev/null || true
    cp -r data/* "$STAGE/usr/share/bison/" 2>/dev/null || true
}

# ── Main ─────────────────────────────────────────────────────────────────

log "Building C toolchain"
log2 "GCC_SRC=$GCC_SRC"
log2 "BINUTILS_SRC=$BINUTILS_SRC"
log2 "STAGE=$STAGE"

mkdir -p "$STAGE"

build_zlib
build_gmp
build_mpfr
build_mpc
build_binutils
build_gcc
build_flex
build_bison

log "C toolchain build complete"
