#!/usr/bin/env bash # # build-montauk-toolchain.sh — Build an -montauk cross compiler # (Binutils + GCC) for MontaukOS userspace programs. # # Usage: ./toolchain/build-montauk-toolchain.sh [x86_64|aarch64] # (or ARCH=aarch64 ./toolchain/build-montauk-toolchain.sh) # # Unlike the bare-metal x86_64-elf toolchain (build-toolchain.sh), this # target knows the Montauk userspace ABI: it links static ET_EXEC # binaries against the Montauk libc (crt1/crti/crtn + libc.a from # programs/lib/libc), defaults to -mno-red-zone, forces 4 KiB segment # alignment for the kernel ELF loader, and defines __montauk__. # # The toolchain is installed to toolchain/local/ and provides: # x86_64-montauk-gcc, x86_64-montauk-g++, x86_64-montauk-ld, etc. # (aarch64-montauk-* for ARCH=aarch64; both can live side by side). # # The target sysroot (libc headers + libs) is assembled into # toolchain/sysroot/ (x86_64) or toolchain/sysroot-/ from # programs/include/libc and programs/lib/libc. # Re-run this script after changing libc headers or rebuilding libc to # refresh the sysroot (the sysroot step always runs; compiler builds # are skipped once installed). # # This script is idempotent — it skips steps whose output already exists. set -euo pipefail # ── Versions (kept in lockstep with build-toolchain.sh) ─────────────────────── BINUTILS_VERSION="2.43.1" GCC_VERSION="14.2.0" ARCH="${1:-${ARCH:-x86_64}}" case "${ARCH}" in x86_64|aarch64) ;; *) echo "Unsupported architecture: ${ARCH}" >&2; exit 1 ;; esac TARGET="${ARCH}-montauk" # ── Directories ─────────────────────────────────────────────────────────────── SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(dirname "${SCRIPT_DIR}")" PREFIX="${SCRIPT_DIR}/local" SRC_DIR="${SCRIPT_DIR}/src" BUILD_DIR="${SCRIPT_DIR}/build" if [[ "${ARCH}" == x86_64 ]]; then SYSROOT="${SCRIPT_DIR}/sysroot" BUILD_SUFFIX="" else SYSROOT="${SCRIPT_DIR}/sysroot-${ARCH}" BUILD_SUFFIX="-${ARCH}" fi PATCH_DIR="${SCRIPT_DIR}/patches" FILES_DIR="${SCRIPT_DIR}/files" BINUTILS_TARBALL="binutils-${BINUTILS_VERSION}.tar.xz" GCC_TARBALL="gcc-${GCC_VERSION}.tar.xz" BINUTILS_URL="https://ftp.gnu.org/gnu/binutils/${BINUTILS_TARBALL}" GCC_URL="https://ftp.gnu.org/gnu/gcc/gcc-${GCC_VERSION}/${GCC_TARBALL}" JOBS="$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)" # ── Color helpers ───────────────────────────────────────────────────────────── bold() { printf '\033[1m%s\033[0m\n' "$*"; } green() { printf '\033[1;32m%s\033[0m\n' "$*"; } red() { printf '\033[1;31m%s\033[0m\n' "$*"; } bold "=== Building ${TARGET} cross compiler ===" bold " Binutils ${BINUTILS_VERSION} | GCC ${GCC_VERSION}" bold " Install prefix: ${PREFIX}" bold " Sysroot: ${SYSROOT}" bold " Parallel jobs: ${JOBS}" echo export PATH="${PREFIX}/bin:${PATH}" mkdir -p "${SRC_DIR}" "${BUILD_DIR}" "${PREFIX}" # ── Download + extract sources (shared with build-toolchain.sh) ─────────────── download() { local url="$1" dest="$2" if [[ -f "${dest}" ]]; then bold "Already downloaded: $(basename "${dest}")" else bold "Downloading $(basename "${dest}")..." wget -q --show-progress -O "${dest}" "${url}" fi } extract() { local tarball="$1" name="$2" if [[ -d "${SRC_DIR}/${name}" ]]; then bold "Already extracted: ${name}" else bold "Extracting ${name}..." tar xf "${tarball}" -C "${SRC_DIR}" fi } download "${BINUTILS_URL}" "${SRC_DIR}/${BINUTILS_TARBALL}" download "${GCC_URL}" "${SRC_DIR}/${GCC_TARBALL}" extract "${SRC_DIR}/${BINUTILS_TARBALL}" "binutils-${BINUTILS_VERSION}" extract "${SRC_DIR}/${GCC_TARBALL}" "gcc-${GCC_VERSION}" if [[ ! -d "${SRC_DIR}/gcc-${GCC_VERSION}/gmp" ]]; then bold "Downloading GCC prerequisites..." (cd "${SRC_DIR}/gcc-${GCC_VERSION}" && ./contrib/download_prerequisites) fi # ── Apply Montauk target patches ────────────────────────────────────────────── apply_patch() { local dir="$1" patch="$2" if grep -q "montauk" "${dir}/config.sub"; then bold "Already patched: $(basename "${dir}")" else bold "Patching $(basename "${dir}")..." patch -d "${dir}" -p1 < "${patch}" fi } # The AArch64 target is a second patch on top of the first. apply_arch_patch() { local dir="$1" patch="$2" marker="$3" if grep -q "aarch64-\*-montauk" "${dir}/${marker}"; then bold "Already patched for aarch64: $(basename "${dir}")" else bold "Patching $(basename "${dir}") for aarch64..." patch -d "${dir}" -p1 < "${patch}" fi } apply_patch "${SRC_DIR}/binutils-${BINUTILS_VERSION}" "${PATCH_DIR}/binutils-${BINUTILS_VERSION}-montauk.patch" apply_patch "${SRC_DIR}/gcc-${GCC_VERSION}" "${PATCH_DIR}/gcc-${GCC_VERSION}-montauk.patch" apply_arch_patch "${SRC_DIR}/binutils-${BINUTILS_VERSION}" \ "${PATCH_DIR}/binutils-${BINUTILS_VERSION}-montauk-aarch64.patch" bfd/config.bfd apply_arch_patch "${SRC_DIR}/gcc-${GCC_VERSION}" \ "${PATCH_DIR}/gcc-${GCC_VERSION}-montauk-aarch64.patch" gcc/config.gcc cp "${FILES_DIR}/montauk.h" "${SRC_DIR}/gcc-${GCC_VERSION}/gcc/config/montauk.h" cp "${FILES_DIR}/aarch64-montauk.h" "${SRC_DIR}/gcc-${GCC_VERSION}/gcc/config/aarch64/montauk.h" # ── Assemble the target sysroot ─────────────────────────────────────────────── # usr/include — the Montauk libc headers (self-contained C; stddef/stdint/ # stdarg come from GCC itself) plus the montauk/ SDK headers. # usr/lib — crt startup objects and the libc static library. LIBC_DIR="${REPO_DIR}/programs/lib/libc" if [[ "${ARCH}" != x86_64 ]]; then LIBC_DIR="${LIBC_DIR}/build-${ARCH}" fi # On a first build for a new architecture the libc cannot be compiled yet # (it needs this compiler): the sysroot then holds only headers, which is # all building GCC needs, and is completed once GCC exists. assemble_sysroot() { bold "Assembling sysroot from programs/lib/libc..." local have_libc=1 make -C "${REPO_DIR}/programs" libc ARCH="${ARCH}" >/dev/null 2>&1 || have_libc=0 [[ -f "${LIBC_DIR}/liblibc.a" && -f "${LIBC_DIR}/crt1.o" ]] || have_libc=0 rm -rf "${SYSROOT}" mkdir -p "${SYSROOT}/usr/include" "${SYSROOT}/usr/lib" cp -r "${REPO_DIR}/programs/include/libc/." "${SYSROOT}/usr/include/" cp -r "${REPO_DIR}/programs/include/montauk" "${SYSROOT}/usr/include/montauk" cp -r "${REPO_DIR}/programs/include/Api" "${SYSROOT}/usr/include/Api" # Freestanding C++ headers (, , , ...) — # the subset the kernel and the in-tree programs build against. This is # NOT the whole C++ story: a hosted libstdc++ is built further down and # installs its own headers under ${PREFIX}/${TARGET}/include/c++. # That directory comes first on the include path and this one comes # last, so a plain g++ invocation gets the hosted headers and these are # shadowed. See "C++ standard library" in README.md. cp -r "${REPO_DIR}/kernel/freestnd-cxx-hdrs/${ARCH}/include/." "${SYSROOT}/usr/include/" if [[ ${have_libc} == 1 ]]; then cp "${LIBC_DIR}/crt1.o" "${LIBC_DIR}/crti.o" "${LIBC_DIR}/crtn.o" "${SYSROOT}/usr/lib/" cp "${LIBC_DIR}/liblibc.a" "${SYSROOT}/usr/lib/libc.a" else bold "libc not built yet - sysroot has headers only." fi # libm is an empty stand-in (math lives inside libc.a; the g++ driver # links -lm unconditionally). libstdc++ is copied from the toolchain # once built - the empty stand-in only bridges the first bootstrap. ar rcs "${SYSROOT}/usr/lib/libm.a" if [[ -f "${PREFIX}/${TARGET}/lib/libstdc++.a" ]]; then cp "${PREFIX}/${TARGET}/lib/libstdc++.a" "${SYSROOT}/usr/lib/libstdc++.a" else ar rcs "${SYSROOT}/usr/lib/libstdc++.a" fi green "Sysroot ready." return 0 } assemble_sysroot # ── Build Binutils ──────────────────────────────────────────────────────────── if [[ -x "${PREFIX}/bin/${TARGET}-ld" ]]; then bold "Binutils already installed — skipping." else bold "Building Binutils ${BINUTILS_VERSION} for ${TARGET}..." rm -rf "${BUILD_DIR}/binutils-montauk${BUILD_SUFFIX}" mkdir -p "${BUILD_DIR}/binutils-montauk${BUILD_SUFFIX}" ( cd "${BUILD_DIR}/binutils-montauk${BUILD_SUFFIX}" "${SRC_DIR}/binutils-${BINUTILS_VERSION}/configure" \ --target="${TARGET}" \ --prefix="${PREFIX}" \ --with-sysroot="${SYSROOT}" \ --disable-nls \ --disable-werror make -j"${JOBS}" make install ) green "Binutils installed." fi # ── Build GCC ───────────────────────────────────────────────────────────────── # Hosted against the Montauk libc headers in the sysroot. Builds the # compiler, libgcc, and a hosted libstdc++ (full STL: string/vector/map, # exceptions, RTTI, iostream). libstdc++ is built gthr-single because # the libc has no pthreads, so / are absent and # shared_ptr uses non-atomic refcounts. See README.md. if [[ -x "${PREFIX}/bin/${TARGET}-gcc" ]]; then bold "GCC already installed — skipping." else bold "Building GCC ${GCC_VERSION} for ${TARGET}..." rm -rf "${BUILD_DIR}/gcc-montauk${BUILD_SUFFIX}" mkdir -p "${BUILD_DIR}/gcc-montauk${BUILD_SUFFIX}" ( cd "${BUILD_DIR}/gcc-montauk${BUILD_SUFFIX}" "${SRC_DIR}/gcc-${GCC_VERSION}/configure" \ --target="${TARGET}" \ --prefix="${PREFIX}" \ --with-sysroot="${SYSROOT}" \ --enable-languages=c,c++ \ --disable-nls \ --disable-shared \ --disable-multilib \ --disable-gcov \ --with-gnu-as \ --with-gnu-ld \ --with-newlib \ --enable-initfini-array \ --disable-wchar_t \ --disable-libstdcxx-pch make -j"${JOBS}" all-gcc make -j"${JOBS}" all-target-libgcc make install-gcc make install-target-libgcc make -j"${JOBS}" all-target-libstdc++-v3 make install-target-libstdc++-v3 ) green "GCC and libstdc++ installed." fi # A first build for this architecture had no libc: complete the sysroot. if [[ ! -f "${SYSROOT}/usr/lib/libc.a" ]] || [[ ! -s "${SYSROOT}/usr/lib/libstdc++.a" ]]; then assemble_sysroot fi # ── Done ────────────────────────────────────────────────────────────────────── echo green "=== ${TARGET} cross compiler ready ===" "${PREFIX}/bin/${TARGET}-gcc" --version | head -1 "${PREFIX}/bin/${TARGET}-ld" --version | head -1 echo bold "Try it:" bold " echo 'int main(){return 0;}' > /tmp/t.c" bold " ${PREFIX}/bin/${TARGET}-gcc /tmp/t.c -o /tmp/t.elf"