#!/usr/bin/env bash # Fast build script for local development - optimized for speed set -euo pipefail # Usage banner usage() { cat </dev/null 2>&1; then printf '%s' "$input" | shasum -a 256 | awk '{print $1}' elif command -v sha256sum >/dev/null 2>&1; then printf '%s' "$input" | sha256sum | awk '{print $1}' else python3 - <<'PY' 2>/dev/null import hashlib, sys data = sys.stdin.read().encode() print(hashlib.sha256(data).hexdigest()) PY fi } sanitize_cache_key() { local raw="${1:-}" # Replace any unsupported characters with '-' raw="${raw//[^A-Za-z0-9._-]/-}" # Collapse repeated separators while [[ "$raw" == *--* ]]; do raw="${raw//--/-}" done # Trim leading and trailing dashes while [[ "$raw" == -* ]]; do raw="${raw#-}" done while [[ "$raw" == *- ]]; do raw="${raw%-}" done if [ -z "$raw" ]; then raw="default" fi # Prevent overly long directory names if [ "${#raw}" -gt 120 ]; then raw="${raw:0:120}" fi printf '%s' "$raw" } bin_requested() { local needle="${1:-}" for candidate in "${TARGET_BINS[@]}"; do if [ "${candidate}" = "${needle}" ]; then return 0 fi done return 1 } resolve_bin_path() { case "$PROFILE" in dev-fast) BIN_SUBDIR="dev-fast" ;; dev) BIN_SUBDIR="debug" ;; *) BIN_SUBDIR="$PROFILE" ;; esac local target_root if [ -n "${CARGO_TARGET_DIR:-}" ]; then target_root="${CARGO_TARGET_DIR}" else target_root="${REPO_ROOT}/${WORKSPACE_DIR}/target" fi if [[ "${target_root}" != /* ]]; then target_root="$(cd "${target_root}" >/dev/null 2>&1 && pwd)" fi TARGET_DIR_ABS="${target_root}" BIN_CARGO_FILENAME="${CRATE_PREFIX}" BIN_FILENAME="${CRATE_PREFIX}" if [ "$PROFILE" = "perf" ]; then BIN_FILENAME="${CRATE_PREFIX}-perf" fi BIN_SUBPATH="${BIN_SUBDIR}/${BIN_FILENAME}" BIN_CARGO_SUBPATH="${BIN_SUBDIR}/${BIN_CARGO_FILENAME}" BIN_PATH="${TARGET_DIR_ABS}/${BIN_SUBPATH}" BIN_CARGO_PATH="${TARGET_DIR_ABS}/${BIN_CARGO_SUBPATH}" BIN_LINK_PATH="./target/${BIN_SUBPATH}" if [ -n "${REPO_TARGET_ABS:-}" ] && [ "${TARGET_DIR_ABS}" = "${REPO_TARGET_ABS}" ]; then BIN_DISPLAY_PATH="./${WORKSPACE_DIR}/target/${BIN_SUBPATH}" else BIN_DISPLAY_PATH="${BIN_PATH}" fi } case "${1:-}" in -h|--help) usage; exit 0 ;; esac RUN_AFTER_BUILD=0 ARG_PROFILE="" WORKSPACE_CHOICE="${WORKSPACE:-}" PASSTHROUGH_ARGS=() while [ $# -gt 0 ]; do case "$1" in run) RUN_AFTER_BUILD=1 PASSTHROUGH_ARGS+=("$1") ;; --workspace) shift || { echo "Error: --workspace requires a value." >&2; usage; exit 1; } WORKSPACE_CHOICE="$1" ;; --workspace=*) WORKSPACE_CHOICE="${1#*=}" ;; -h|--help) usage exit 0 ;; *) if [ -n "$ARG_PROFILE" ]; then echo "Error: Multiple profile arguments provided ('${ARG_PROFILE}' and '$1')." >&2 usage exit 1 fi ARG_PROFILE="$1" PASSTHROUGH_ARGS+=("$1") ;; esac shift done if [ -z "$WORKSPACE_CHOICE" ]; then WORKSPACE_CHOICE="dev" fi if [ "$ARG_PROFILE" = "pref" ]; then ARG_PROFILE="perf" fi # Resolve repository paths relative to this script so absolute invocation works SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" if [ -n "${HANZO_CALLER_CWD:-}" ]; then if ! CALLER_CWD="$(cd "${HANZO_CALLER_CWD}" >/dev/null 2>&1 && pwd)"; then echo "Error: HANZO_CALLER_CWD is not a valid directory: ${HANZO_CALLER_CWD}" >&2 exit 1 fi else CALLER_CWD="$(pwd)" fi if [[ "${SCRIPT_DIR}" == */.code/working/*/branches/* ]]; then WORKTREE_PARENT="${SCRIPT_DIR%/branches/*}" REPO_NAME="$(basename "${WORKTREE_PARENT}")" else REPO_NAME="$(basename "${SCRIPT_DIR}")" fi REPO_ROOT="${SCRIPT_DIR}" # Guard against regressions where a hanzo-dev crate references ../codex-rs. if [ "${BUILD_FAST_SKIP_CODEX_GUARD:-0}" != "1" ]; then echo "Running codex path dependency guard..." ( cd "$REPO_ROOT" scripts/check-codex-path-deps.sh ) fi if [ "$WORKSPACE_CHOICE" = "both" ]; then if [ "$RUN_AFTER_BUILD" -eq 1 ]; then echo "Error: --workspace both cannot be combined with 'run'." >&2 exit 1 fi for ws in codex code; do WORKSPACE="$ws" "$0" "${PASSTHROUGH_ARGS[@]}" --workspace "$ws" done exit 0 fi if [ -n "${HANZO_HOME:-}" ] && [ -n "${HANZO_HOME}" ]; then CACHE_HOME="${HANZO_HOME%/}" elif [ -n "${CODEX_HOME:-}" ] && [ -n "${CODEX_HOME}" ]; then CACHE_HOME="${CODEX_HOME%/}" else if [ -d "/mnt/data" ] && [ -w "/mnt/data" ]; then CACHE_HOME="/mnt/data/.hanzo" else CACHE_HOME="${REPO_ROOT}/.hanzo" fi fi case "${CACHE_HOME}" in /*) ;; *) CACHE_HOME="${REPO_ROOT}/${CACHE_HOME#./}" ;; esac case "$WORKSPACE_CHOICE" in codex|codex-rs) WORKSPACE_DIR="codex-rs" CRATE_PREFIX="codex" ;; dev|hanzo-dev) WORKSPACE_DIR="hanzo-dev" CRATE_PREFIX="dev" ;; *) echo "Error: Unknown workspace '${WORKSPACE_CHOICE}'. Use codex, dev, or both." >&2 exit 1 ;; esac WORKSPACE_PATH="${SCRIPT_DIR}/${WORKSPACE_DIR}" if [ ! -d "$WORKSPACE_PATH" ]; then echo "Error: Workspace directory '${WORKSPACE_PATH}' not found." >&2 exit 1 fi TARGET_CACHE_ROOT="${CACHE_HOME}/working/_target-cache/${REPO_NAME}" # Change to the selected Rust workspace root regardless of caller CWD cd "${WORKSPACE_PATH}" WORKTREE_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" if [ -z "${BUILD_FAST_CACHE_KEY:-}" ]; then if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then BRANCH_NAME_RAW="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo HEAD)" if [ "${BRANCH_NAME_RAW}" = "HEAD" ]; then BRANCH_NAME_RAW="detached-$(git rev-parse --short HEAD 2>/dev/null || date +%Y%m%d%H%M%S)" fi else BRANCH_NAME_RAW="unknown" fi BRANCH_FINGERPRINT="$(hash_string "${BRANCH_NAME_RAW}")" BRANCH_HASH_SHORT="${BRANCH_FINGERPRINT:0:12}" WORKTREE_HASH="$(hash_string "${WORKTREE_ROOT}")" WORKTREE_HASH_SHORT="${WORKTREE_HASH:0:12}" CACHE_KEY_RAW="${BRANCH_NAME_RAW}-${BRANCH_HASH_SHORT}-${WORKTREE_HASH_SHORT}" CACHE_KEY_SOURCE="branch/worktree" else CACHE_KEY_RAW="${BUILD_FAST_CACHE_KEY}" CACHE_KEY_SOURCE="override" fi CACHE_KEY="$(sanitize_cache_key "${CACHE_KEY_RAW}")" if [ -z "${CACHE_KEY}" ]; then CACHE_KEY="default" fi TARGET_CACHE_DIR="${TARGET_CACHE_ROOT}/${CACHE_KEY}/${WORKSPACE_DIR}" if [ -z "${CARGO_TARGET_DIR:-}" ]; then TARGET_CACHE_DIR_ABS="${TARGET_CACHE_DIR}" case "${TARGET_CACHE_DIR_ABS}" in /*) ;; *) TARGET_CACHE_DIR_ABS="${REPO_ROOT}/${TARGET_CACHE_DIR_ABS#./}" ;; esac mkdir -p "${TARGET_CACHE_DIR_ABS}" 2>/dev/null || true export CARGO_TARGET_DIR="${TARGET_CACHE_DIR_ABS}" else TARGET_CACHE_DIR_ABS="${CARGO_TARGET_DIR}" fi echo "Cache bucket: ${CACHE_KEY} (${CACHE_KEY_SOURCE})" CLI_PACKAGE="$(sed -En 's/^name[[:space:]]*=[[:space:]]*"(.*)"/\1/p' cli/Cargo.toml | head -n1)" TUI_PACKAGE="$(sed -En 's/^name[[:space:]]*=[[:space:]]*"(.*)"/\1/p' tui/Cargo.toml | head -n1)" EXEC_PACKAGE="$(sed -En 's/^name[[:space:]]*=[[:space:]]*"(.*)"/\1/p' exec/Cargo.toml | head -n1)" PRIMARY_CLI_BIN="$(awk 'BEGIN{inbin=0} /^\[\[bin\]\]/{inbin=1; next} inbin && /^name[[:space:]]*=/{gsub(/.*"/,"",$0); gsub(/"/,"",$0); print; exit}' cli/Cargo.toml)" if [ -n "${PRIMARY_CLI_BIN}" ]; then CRATE_PREFIX="${PRIMARY_CLI_BIN}" else CRATE_PREFIX="${CLI_PACKAGE##*-}" fi EXEC_BIN="$(awk 'BEGIN{inbin=0} /^\[\[bin\]\]/{inbin=1; next} inbin && /^name[[:space:]]*=/{gsub(/.*"/,"",$0); gsub(/"/,"",$0); print; exit}' exec/Cargo.toml)" if [ -z "${EXEC_BIN}" ]; then EXEC_BIN="${EXEC_PACKAGE}" fi TARGET_BINS=() if [ -n "${BUILD_FAST_BINS:-}" ]; then for raw_bin in ${BUILD_FAST_BINS//,/ }; do bin_candidate="$(trim "$raw_bin")" if [ -n "${bin_candidate}" ]; then TARGET_BINS+=("${bin_candidate}") fi done fi if [ "${#TARGET_BINS[@]}" -eq 0 ]; then TARGET_BINS=("${CRATE_PREFIX}") fi PRIMARY_PRESENT=0 for candidate in "${TARGET_BINS[@]}"; do if [ "${candidate}" = "${CRATE_PREFIX}" ]; then PRIMARY_PRESENT=1 break fi done if [ "$PRIMARY_PRESENT" -eq 0 ]; then TARGET_BINS=("${CRATE_PREFIX}" "${TARGET_BINS[@]}") fi PRIMARY_BIN="${TARGET_BINS[0]}" # Default to preserving caller environment unless explicitly disabled KEEP_ENV="${KEEP_ENV:-1}" # Track whether the caller explicitly set PROFILE (env or CLI) PROFILE_ENV_SUPPLIED=0 if [ -n "${PROFILE+x}" ]; then PROFILE_ENV_SUPPLIED=1 PROFILE_VALUE="$PROFILE" else PROFILE_VALUE="dev-fast" fi if [ -n "$ARG_PROFILE" ]; then PROFILE_VALUE="$ARG_PROFILE" fi PROFILE_EXPLICIT=0 if [ "$PROFILE_ENV_SUPPLIED" -eq 1 ] || [ -n "$ARG_PROFILE" ]; then PROFILE_EXPLICIT=1 fi PROFILE="$PROFILE_VALUE" # Optional deterministic mode: aim for more stable hashes by removing # UUIDs on macOS, disabling debuginfo, and preferring a single-codegen # optimized profile when user hasn't explicitly chosen a profile. if [ "${DETERMINISTIC:-}" = "1" ]; then echo "Deterministic build: enabled" DET_FORCE_REL="${DETERMINISTIC_FORCE_RELEASE:-1}" if [ "$PROFILE" = "dev-fast" ] && [ "$DET_FORCE_REL" = "1" ]; then PROFILE="release-prod" echo "Deterministic build: switching profile to ${PROFILE}" elif [ "$PROFILE" = "dev-fast" ]; then echo "Deterministic build: keeping profile ${PROFILE} (DETERMINISTIC_FORCE_RELEASE=0)" fi # Use SOURCE_DATE_EPOCH if available to stabilize timestamps if command -v git >/dev/null 2>&1 && git -C "$REPO_ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then export SOURCE_DATE_EPOCH="$(git -C "$REPO_ROOT" log -1 --pretty=%ct 2>/dev/null || true)" fi # Keep debuginfo intact so profiling tools can resolve symbols. fi ORIGINAL_PROFILE="$PROFILE" if [ "$PROFILE" != "dev" ] && [ "$PROFILE" != "release" ]; then if ! grep -F "[profile.${PROFILE}]" Cargo.toml >/dev/null 2>&1; then case "$PROFILE" in dev-fast) PROFILE="dev" ;; perf|release-prod) PROFILE="release" ;; *) PROFILE="dev" ;; esac if [ "$ORIGINAL_PROFILE" != "$PROFILE" ]; then echo "Profile ${ORIGINAL_PROFILE} not defined in ${WORKSPACE_DIR}/Cargo.toml; falling back to ${PROFILE}." fi fi fi # Select the cargo/rustc toolchain to match deploy # Prefer rustup with the toolchain pinned in rust-toolchain.toml or $RUSTUP_TOOLCHAIN USE_CARGO="cargo" if command -v rustup >/dev/null 2>&1; then # Determine desired toolchain if [ -n "${RUSTUP_TOOLCHAIN:-}" ]; then TOOLCHAIN="$RUSTUP_TOOLCHAIN" else # Try parse channel from rust-toolchain.toml in repo root if [ -f "rust-toolchain.toml" ]; then TOOLCHAIN=$(sed -n 's/^channel\s*=\s*"\(.*\)"/\1/p' rust-toolchain.toml | head -n1) fi # Fallback to active default if none found TOOLCHAIN="${TOOLCHAIN:-$(rustup show active-toolchain 2>/dev/null | awk '{print $1}')}" fi if [ -n "$TOOLCHAIN" ]; then # Ensure toolchain is installed; if not, attempt install quietly if ! rustup which rustc --toolchain "$TOOLCHAIN" >/dev/null 2>&1; then echo "rustup: installing toolchain $TOOLCHAIN ..." rustup toolchain install "$TOOLCHAIN" >/dev/null 2>&1 || true fi USE_CARGO="rustup run $TOOLCHAIN cargo" echo "Using rustup toolchain: $TOOLCHAIN" rustup run "$TOOLCHAIN" rustc --version || true else echo "rustup found but no toolchain detected; using system cargo" fi else echo "Error: rustup is required for consistent builds." echo "Please install rustup: https://rustup.rs/" exit 1 fi # Canonicalize build environment only when requested. # Set KEEP_ENV=0 to force sanitization. if [ "${KEEP_ENV}" != "1" ]; then # Only define RUSTFLAGS via feature flags like DETERMINISTIC=1; otherwise clear. if [ -z "${DETERMINISTIC:-}" ]; then export RUSTFLAGS="" fi unset RUSTC_WRAPPER CARGO_BUILD_RUSTC_WRAPPER SCCACHE SCCACHE_BIN CARGO_TARGET_DIR MACOSX_DEPLOYMENT_TARGET unset CARGO_PROFILE_RELEASE_LTO CARGO_PROFILE_DEV_FAST_LTO CARGO_PROFILE_RELEASE_CODEGEN_UNITS CARGO_PROFILE_DEV_FAST_CODEGEN_UNITS # Ensure incremental uses profile default unset CARGO_INCREMENTAL CANONICAL_ENV_APPLIED=1 else CANONICAL_ENV_APPLIED=0 fi if [ -z "${CARGO_TARGET_DIR:-}" ]; then export CARGO_TARGET_DIR="${TARGET_CACHE_DIR_ABS}" fi if command -v sccache >/dev/null 2>&1; then if [ -z "${RUSTC_WRAPPER:-}" ]; then export RUSTC_WRAPPER="$(command -v sccache)" fi if [ -z "${SCCACHE_DIR:-}" ]; then export SCCACHE_DIR="${CACHE_HOME}/sccache" fi if [ -z "${SCCACHE_CACHE_SIZE:-}" ]; then export SCCACHE_CACHE_SIZE="50G" fi mkdir -p "${SCCACHE_DIR}" 2>/dev/null || true fi # Optional debug symbol override for profiling sessions if [ "${DEBUG_SYMBOLS:-}" = "1" ]; then if [ "$PROFILE" = "perf" ]; then echo "Debug symbols: profile 'perf' already preserves debuginfo" elif [ "$PROFILE_EXPLICIT" -eq 0 ] && [ "$PROFILE" = "dev-fast" ]; then echo "Debug symbols requested: switching profile to perf" PROFILE="perf" else PROFILE_ENV_KEY="$(printf "%s" "$PROFILE" | tr '[:lower:]-' '[:upper:]_')" DEBUG_VAR="CARGO_PROFILE_${PROFILE_ENV_KEY}_DEBUG" STRIP_VAR="CARGO_PROFILE_${PROFILE_ENV_KEY}_STRIP" SPLIT_VAR="CARGO_PROFILE_${PROFILE_ENV_KEY}_SPLIT_DEBUGINFO" printf -v "$DEBUG_VAR" '%s' '2' printf -v "$STRIP_VAR" '%s' 'none' printf -v "$SPLIT_VAR" '%s' 'packed' export "$DEBUG_VAR" "$STRIP_VAR" "$SPLIT_VAR" echo "Debug symbols: forcing debuginfo for profile ${PROFILE}" fi if [ -n "${RUSTFLAGS:-}" ]; then CLEAN_RUSTFLAGS="${RUSTFLAGS//-C debuginfo=0/}" CLEAN_RUSTFLAGS="${CLEAN_RUSTFLAGS// / }" CLEAN_RUSTFLAGS="${CLEAN_RUSTFLAGS## }" CLEAN_RUSTFLAGS="${CLEAN_RUSTFLAGS%% }" export RUSTFLAGS="${CLEAN_RUSTFLAGS}" fi export CARGO_PROFILE_RELEASE_STRIP="none" export CARGO_PROFILE_RELEASE_PROD_STRIP="none" fi # Auto-detect version from git tag when CODE_VERSION is not set if [ -z "${CODE_VERSION:-}" ]; then GIT_TAG_VERSION="$(git describe --tags --abbrev=0 2>/dev/null || true)" if [ -n "${GIT_TAG_VERSION}" ]; then export CODE_VERSION="${GIT_TAG_VERSION#v}" echo "Version: ${CODE_VERSION} (from git tag ${GIT_TAG_VERSION})" fi fi # Keep compile-time build metadata in sync with the current checkout so # `dev --version` can always be matched back to git tag/hash/date. if [ -z "${CODE_GIT_DESCRIBE:-}" ]; then CODE_GIT_DESCRIBE="$(git describe --tags --always --dirty 2>/dev/null || true)" if [ -n "${CODE_GIT_DESCRIBE}" ]; then export CODE_GIT_DESCRIBE fi fi if [ -z "${CODE_GIT_SHA:-}" ]; then CODE_GIT_SHA="$(git rev-parse --short=12 HEAD 2>/dev/null || true)" if [ -n "${CODE_GIT_SHA}" ]; then export CODE_GIT_SHA fi fi if [ -z "${CODE_GIT_DATE:-}" ]; then CODE_GIT_DATE="$(git show -s --format=%cs HEAD 2>/dev/null || true)" if [ -n "${CODE_GIT_DATE}" ]; then export CODE_GIT_DATE fi fi if [ -n "${CODE_GIT_DESCRIBE:-}" ] || [ -n "${CODE_GIT_SHA:-}" ] || [ -n "${CODE_GIT_DATE:-}" ]; then echo "Build metadata: ${CODE_GIT_DESCRIBE:-unknown} ${CODE_GIT_SHA:+(${CODE_GIT_SHA})} ${CODE_GIT_DATE:+${CODE_GIT_DATE}}" fi echo "Building ${CRATE_PREFIX} binary (${PROFILE} mode)..." # Ensure Cargo cache locations are stable. # In CI, we can optionally enforce a specific CARGO_HOME regardless of caller env # by setting STRICT_CARGO_HOME=1 (used by Issue Triage workflow to keep caching deterministic). # Cargo/rustup home directories # When STRICT_CARGO_HOME=1, honor CARGO_HOME_ENFORCED if provided; otherwise # force a per-repo location to avoid HOME permission/caching issues on CI. if [ "${STRICT_CARGO_HOME:-}" = "1" ]; then export CARGO_HOME="${CARGO_HOME_ENFORCED:-${REPO_ROOT}/.cargo-home}" else # Default: write inside workspace if not set to avoid HOME permission issues if [ -z "${CARGO_HOME:-}" ]; then export CARGO_HOME="${REPO_ROOT}/.cargo-home" fi fi # Keep rustup’s home alongside Cargo by default when not explicitly set if [ -z "${RUSTUP_HOME:-}" ]; then export RUSTUP_HOME="${CARGO_HOME%/}/rustup" fi if [ -z "${CARGO_TARGET_DIR:-}" ]; then export CARGO_TARGET_DIR="${WORKSPACE_PATH}/target" fi mkdir -p "${CARGO_HOME}" "${CARGO_TARGET_DIR}" 2>/dev/null || true # Ensure repo-local target directory exists for compatibility symlinks mkdir -p ./target REPO_TARGET_ABS="$(cd ./target >/dev/null 2>&1 && pwd)" resolve_bin_path # Use sparse registry for faster index updates when available export CARGO_REGISTRIES_CRATES_IO_PROTOCOL="sparse" # Resolve actual cargo/rustc binaries that will be used (via rustup) for fingerprinting REAL_CARGO_BIN="$(rustup which cargo 2>/dev/null || command -v cargo || echo cargo)" REAL_RUSTC_BIN="$(rustup which rustc 2>/dev/null || command -v rustc || echo rustc)" # Determine current host triple for dynamic symlink naming HOST_TRIPLE="$(rustup run "$TOOLCHAIN" rustc -vV 2>/dev/null | awk -F': ' '/^host: /{print $2}')" TRIPLE="${HOST_TRIPLE:-}" if [ -z "$TRIPLE" ]; then # Fallback for Darwin when rustup is unavailable for some reason if [ "$(uname -s)" = "Darwin" ]; then TRIPLE="$(uname -m)-apple-darwin" [ "$TRIPLE" = "arm64-apple-darwin" ] && TRIPLE="aarch64-apple-darwin" else TRIPLE="unknown-unknown-unknown" fi fi # Check Cargo.lock validity (fast, non-blocking check) using the selected cargo if ! CARGO_HOME="$CARGO_HOME" RUSTUP_HOME="$RUSTUP_HOME" ${USE_CARGO} metadata --locked --format-version 1 >/dev/null 2>&1; then echo "⚠️ Warning: Cargo.lock appears out of date or inconsistent" echo " This might mean:" echo " • You've modified Cargo.toml dependencies" echo " • You've changed workspace crate versions" echo " • The lockfile is missing entries" echo "" echo " Run 'cargo update' to update all dependencies, or" echo " Run 'cargo update -p ' to update specific crates" echo "" echo " Continuing with unlocked build for development..." echo "" USE_LOCKED="" else # Lockfile is valid, use it for consistent builds USE_LOCKED="--locked" fi # Optional trace mode for diagnosing environment differences if [ "${TRACE_BUILD:-}" = "1" ]; then echo "--- TRACE_BUILD environment ---" echo "whoami: $(whoami)" echo "pwd: $(pwd)" echo "SHELL: ${SHELL:-}" bash --version | head -n1 || true if [ -n "${TOOLCHAIN:-}" ]; then echo "TOOLCHAIN: ${TOOLCHAIN}" rustup run "$TOOLCHAIN" rustc -vV || true rustup run "$TOOLCHAIN" cargo -vV || true fi echo "CANONICAL_ENV_APPLIED: ${CANONICAL_ENV_APPLIED} (KEEP_ENV=${KEEP_ENV})" echo "Filtered env (CARGO|RUST*|PROFILE|HANZO_HOME|CODEX_HOME):" env | egrep '^(CARGO|RUST|RUSTUP|PROFILE|HANZO_HOME|CODEX_HOME)=' | sort || true echo "--------------------------------" fi # Compute and compare build cache fingerprint to explain incremental behavior FPRINT_FILE="./target/${PROFILE}/.env-fingerprint" # Collect fingerprint inputs (only env/toolchain/settings that affect codegen/caches) collect_fingerprint() { local cargo_v rustc_v host which_cargo which_rustc uname_srm cargo_v="$(CARGO_HOME="$CARGO_HOME" RUSTUP_HOME="$RUSTUP_HOME" ${USE_CARGO} -V 2>/dev/null || true)" rustc_v="$(rustup run "$TOOLCHAIN" rustc -vV 2>/dev/null || true)" host="$(printf "%s\n" "$rustc_v" | awk -F': ' '/^host: /{print $2}' || true)" which_cargo="${REAL_CARGO_BIN}" which_rustc="${REAL_RUSTC_BIN}" uname_srm="$(uname -srm 2>/dev/null || true)" cat </dev/null | awk '{print $1}')" FPRINT_CHANGED="0" if [ -f "$FPRINT_FILE" ]; then OLD_FPRINT_HASH="$(sed -n 's/^HASH=//p' "$FPRINT_FILE" 2>/dev/null | head -n1)" if [ "${OLD_FPRINT_HASH:-}" != "$NEW_FPRINT_HASH" ]; then FPRINT_CHANGED="1" echo "⚠️ Build cache fingerprint changed since last run for profile '${PROFILE}'." echo " This can trigger incremental rebuilds the first time you build." if [ "${TRACE_BUILD:-}" = "1" ]; then echo "--- previous fingerprint (hash: ${OLD_FPRINT_HASH:-none}) ---"; sed -n '1,200p' "$FPRINT_FILE" 2>/dev/null | sed '1d'; echo "--------------------------------" echo "--- current fingerprint (hash: ${NEW_FPRINT_HASH}) ---"; printf "%s\n" "$NEW_FPRINT_TEXT"; echo "--------------------------------" else echo " Run with TRACE_BUILD=1 to see detailed differences." fi fi fi # Build for native target (no --target flag) for maximum speed # This reuses the host stdlib and normal cache # Determine exec binary name based on workspace metadata if [ -z "${EXEC_BIN}" ]; then EXEC_BIN="${CRATE_PREFIX}-exec" fi # Build with or without --locked based on lockfile validity # Keep stderr and stdout separate so downstream tools can capture both streams. CARGO_BIN_ARGS=() for bin in "${TARGET_BINS[@]}"; do CARGO_BIN_ARGS+=("--bin" "${bin}") done echo "Building bins: ${TARGET_BINS[*]}" ${USE_CARGO} build ${USE_LOCKED} --profile "${PROFILE}" "${CARGO_BIN_ARGS[@]}" # Check if build succeeded if [ $? -eq 0 ]; then resolve_bin_path if [ "$PROFILE" = "perf" ]; then PERF_SOURCE="${BIN_CARGO_PATH}" PERF_TARGET="${BIN_PATH}" if [ -e "${PERF_SOURCE}" ]; then PERF_DIR="$(dirname "${PERF_TARGET}")" mkdir -p "${PERF_DIR}" if [ -e "${PERF_TARGET}" ] || [ -L "${PERF_TARGET}" ]; then rm -f "${PERF_TARGET}" fi ( cd "${PERF_DIR}" >/dev/null 2>&1 ln -sf "$(basename "${PERF_SOURCE}")" "$(basename "${PERF_TARGET}")" ) fi fi echo "✅ Build successful!" echo "Binary location: ${BIN_DISPLAY_PATH}" echo "" # Keep old symlink locations working for compatibility # Create symlink in target/release for npm wrapper expectations release_link_target="../${BIN_SUBDIR}/${BIN_FILENAME}" dev_fast_link_target="../${BIN_SUBDIR}/${BIN_FILENAME}" SYMLINK_PREFIXES=("${CRATE_PREFIX}") if [ "${CRATE_PREFIX}" = "dev" ]; then SYMLINK_PREFIXES+=("hanzo-dev") fi create_cli_symlinks() { local cli_dir="$1" local default_target="$2" mkdir -p "$cli_dir" local link_target="$default_target" if [ -n "${CLI_LINK_ABSOLUTE}" ]; then link_target="${CLI_LINK_ABSOLUTE}" fi for PREFIX in "${SYMLINK_PREFIXES[@]}"; do local dest="${cli_dir}/${PREFIX}-${TRIPLE}" [ -e "$dest" ] && rm -f "$dest" ln -sf "${link_target}" "$dest" done for PREFIX in "${SYMLINK_PREFIXES[@]}"; do local dest="${cli_dir}/${PREFIX}-aarch64-apple-darwin" [ -e "$dest" ] && rm -f "$dest" ln -sf "${link_target}" "$dest" done } CLI_TARGET_CODE="../../target/${BIN_SUBDIR}/${BIN_FILENAME}" CLI_TARGET_CODEX="../../${WORKSPACE_DIR}/target/${BIN_SUBDIR}/${BIN_FILENAME}" CLI_LINK_ABSOLUTE="" if [ "${TARGET_DIR_ABS}" != "${REPO_TARGET_ABS}" ]; then release_link_target="${BIN_PATH}" dev_fast_link_target="${BIN_PATH}" CLI_LINK_ABSOLUTE="${BIN_PATH}" # Maintain repo-local path for downstream tooling when target dir is external if [ -n "${BIN_LINK_PATH:-}" ]; then mkdir -p "$(dirname "${BIN_LINK_PATH}")" if [ -e "${BIN_LINK_PATH}" ]; then rm -f "${BIN_LINK_PATH}" fi ln -sf "${BIN_PATH}" "${BIN_LINK_PATH}" fi fi mkdir -p ./target/release if [ -e "./target/release/${CRATE_PREFIX}" ]; then rm -f "./target/release/${CRATE_PREFIX}" fi ln -sf "${release_link_target}" "./target/release/${CRATE_PREFIX}" # Update the symlinks in CLI wrapper directories if [ -d "../codex-cli/bin" ]; then create_cli_symlinks "../codex-cli/bin" "${CLI_TARGET_CODEX}" fi if [ -d "./dev-cli/bin" ]; then create_cli_symlinks "./dev-cli/bin" "${CLI_TARGET_CODE}" fi BIN_DIR="./bin" mkdir -p "${BIN_DIR}" BIN_DIR_ABS="$(cd "${BIN_DIR}" >/dev/null 2>&1 && pwd)" for BIN_NAME in "${TARGET_BINS[@]}"; do BIN_TARGET_PATH="${TARGET_DIR_ABS}/${BIN_SUBDIR}/${BIN_NAME}" if [ -e "${BIN_TARGET_PATH}" ]; then # Use a per-process temp file to avoid races when multiple build-fast # invocations run concurrently in the same repo. TMP_BIN_PATH="${BIN_DIR}/${BIN_NAME}.tmp.${BASHPID:-$$}" rm -f "${TMP_BIN_PATH}" 2>/dev/null || true cp -f "${BIN_TARGET_PATH}" "${TMP_BIN_PATH}" mv -f "${TMP_BIN_PATH}" "${BIN_DIR}/${BIN_NAME}" chmod +x "${BIN_DIR}/${BIN_NAME}" 2>/dev/null || true fi done RUN_BIN_PATH="${BIN_DIR_ABS}/${PRIMARY_BIN}" # Install primary binary to ~/.local/bin/dev for local testing. # Use a symlink instead of copying the executable. Some macOS setups # can terminate copied binaries in ~/.local/bin with SIGKILL while the # original artifact runs normally. LOCAL_BIN_DIR="${HOME}/.local/bin" mkdir -p "${LOCAL_BIN_DIR}" LOCAL_DEV_BIN="${LOCAL_BIN_DIR}/dev" LOCAL_DEV_SOURCE="${TARGET_DIR_ABS}/${BIN_SUBDIR}/${PRIMARY_BIN}" if [ ! -e "${LOCAL_DEV_SOURCE}" ] && [ -e "${BIN_DIR}/${PRIMARY_BIN}" ]; then LOCAL_DEV_SOURCE="${BIN_DIR_ABS}/${PRIMARY_BIN}" fi if [ -e "${LOCAL_DEV_SOURCE}" ]; then rm -f "${LOCAL_DEV_BIN}" ln -sf "${LOCAL_DEV_SOURCE}" "${LOCAL_DEV_BIN}" echo "Installed: ${LOCAL_DEV_BIN}" fi # Ensure repo-local developer alias stays mapped to latest build output # so the user's `${CRATE_PREFIX}-dev` alias keeps working when pointing at target/dev-fast/${CRATE_PREFIX} # Only create this symlink if we're not already building in dev-fast profile if [ "$PROFILE" != "dev-fast" ]; then mkdir -p ./target/dev-fast if [ -e "./target/dev-fast/${CRATE_PREFIX}" ]; then rm -f "./target/dev-fast/${CRATE_PREFIX}" fi ln -sf "${dev_fast_link_target}" "./target/dev-fast/${CRATE_PREFIX}" fi # Optional post-link step for deterministic builds: re-link executables # with -no_uuid only on macOS. Apply per-bin via `cargo rustc` so # dependencies/proc-macro dylibs are not affected. if [ "${DETERMINISTIC_NO_UUID:-}" = "1" ] && [ "$(uname -s)" = "Darwin" ]; then echo "Deterministic post-link: removing LC_UUID from executables" if bin_requested "${CRATE_PREFIX}"; then ${USE_CARGO} rustc ${USE_LOCKED} --profile "${PROFILE}" -p "${CLI_PACKAGE}" --bin "${CRATE_PREFIX}" -- -C link-arg=-Wl,-no_uuid || true fi if bin_requested "${CRATE_PREFIX}-tui"; then ${USE_CARGO} rustc ${USE_LOCKED} --profile "${PROFILE}" -p "${TUI_PACKAGE}" --bin "${CRATE_PREFIX}-tui" -- -C link-arg=-Wl,-no_uuid || true fi if bin_requested "${EXEC_BIN}"; then ${USE_CARGO} rustc ${USE_LOCKED} --profile "${PROFILE}" -p "${EXEC_PACKAGE}" --bin "${EXEC_BIN}" -- -C link-arg=-Wl,-no_uuid || true fi fi # Compute absolute path and SHA256 for clarity (after any post-linking) ABS_BIN_PATH="${BIN_PATH}" if [[ "${ABS_BIN_PATH}" != /* ]]; then ABS_BIN_PATH="$(cd "$(dirname "${ABS_BIN_PATH}")" >/dev/null 2>&1 && pwd)/$(basename "${ABS_BIN_PATH}")" fi BIN_SHA="" if [ -e "${ABS_BIN_PATH}" ]; then if command -v shasum >/dev/null 2>&1; then BIN_SHA="$(shasum -a 256 "${ABS_BIN_PATH}" | awk '{print $1}')" elif command -v sha256sum >/dev/null 2>&1; then BIN_SHA="$(sha256sum "${ABS_BIN_PATH}" | awk '{print $1}')" fi fi if [ -n "$BIN_SHA" ]; then echo "Binary Hash: ${BIN_SHA} ($(du -sh "${ABS_BIN_PATH}" | awk '{print $1}'))" elif [ -e "${ABS_BIN_PATH}" ]; then echo "Binary Size: $(du -h "${ABS_BIN_PATH}" | cut -f1)" else echo "Binary artifact not found at ${ABS_BIN_PATH}" fi if [ "$RUN_AFTER_BUILD" -eq 1 ]; then RUN_PATH="${RUN_BIN_PATH}" if [ ! -x "${RUN_PATH}" ]; then RUN_PATH="${TARGET_DIR_ABS}/${BIN_SUBDIR}/${PRIMARY_BIN}" fi if [ ! -x "${RUN_PATH}" ]; then echo "❌ Run failed: ${RUN_PATH} is missing or not executable" exit 1 fi echo "Running ${RUN_PATH} (cwd: ${CALLER_CWD})..." ( cd "${CALLER_CWD}" && "${RUN_PATH}" ) RUN_STATUS=$? if [ $RUN_STATUS -ne 0 ]; then echo "❌ Run failed with status ${RUN_STATUS}" exit $RUN_STATUS fi fi if [ "${TRACE_BUILD:-}" = "1" ] && [ -n "$BIN_SHA" ]; then echo "--- TRACE_BUILD artifact ---" echo "ABS_BIN_PATH: ${ABS_BIN_PATH}" echo "SHA256: ${BIN_SHA}" echo "--------------------------------" fi # Persist the current fingerprint to explain future behavior mkdir -p "./target/${PROFILE}" 2>/dev/null || true { echo "HASH=${NEW_FPRINT_HASH}" printf "%s\n" "$NEW_FPRINT_TEXT" } >"$FPRINT_FILE" if [ "$FPRINT_CHANGED" = "1" ]; then echo "🧰 Cache normalized to current environment (fingerprint ${NEW_FPRINT_HASH})." fi # If lockfile was out of date, remind user if [ -z "$USE_LOCKED" ]; then echo "" echo "⚠️ Remember: Built without --locked due to Cargo.lock issues" echo " Consider running 'cargo update' and committing the changes" fi else echo "❌ Build failed" exit 1 fi