Files
emulator/build-citron-linux.sh
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1406 lines
62 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
# SPDX-FileCopyrightText: 2026 citron Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
# =============================================================================
# build-citron-linux.sh — Local Linux build script with PGO + LTO + BOLT
#
# Lives in the citron repo root. Assumes the repo has already been cloned
# (with or without submodules — CPM handles all library dependencies and
# FFmpeg source at cmake configure time; no submodule init is required).
#
# PGO FLAG MANAGEMENT:
# This script manages all PGO and LTO compiler/linker flags directly, using
# CITRON_PGO_FLAGS_MANAGED_BY_SCRIPT=ON to suppress PGO.cmake's built-in
# flag injection.
#
# Why: PGO.cmake's default Clang path uses -fprofile-instr-generate (FE PGO
# only), has no path control, and never passes -fprofile-use to the linker
# command line. Without -fprofile-use on the linker, the LTO backend runs
# without profile guidance even when full LTO is active — negating most of
# the combined PGO+LTO benefit.
#
# This script passes explicit flags to CMAKE_C_FLAGS_<BT>,
# CMAKE_CXX_FLAGS_<BT>, and CMAKE_EXE_LINKER_FLAGS_<BT> so:
# - IR PGO uses -fprofile-generate / -fprofile-use (not -fprofile-instr-*)
# - Profraw files land in a controlled directory with %p PID expansion
# - The LTO linker backend receives -fprofile-use for LTCG with profile data
# - Profile runtime symbols are force-kept on the linker command line
#
# LTO + PGO LINKER FLAGS (use/csgenerate/bolt stages):
# For full LTO (-flto), the -fprofile-use flag must appear on
# CMAKE_EXE_LINKER_FLAGS as well as the compile flags, so the LTO backend
# applies the profile during link-time code generation.
#
# CS-IRPGO (csgenerate stage):
# IR PGO only. Layers context-sensitive counters on top of a stage1-optimized
# binary. Provides per-call-site counter data vs per-function-definition.
# CRITICAL INVARIANT: csgenerate always uses default.profdata (stage1 only)
# as -fprofile-use input, never merged.profdata. If merged.profdata (which
# already contains CS records) were used, the inlining decisions would shift
# relative to the plain stage1 baseline, restructuring the IR that the new CS
# counters are keyed to — yielding profile hash mismatches at the use stage.
#
# STAGES:
# setup Detect your distro and install required build tools (run once).
# generate Stage 1: PGO instrumented build.
# csgenerate Stage 1b: [IR PGO only] CS-IRPGO instrumented build.
# merge Merge collected .profraw files into .profdata.
# summary Show profile statistics.
# use Stage 2: PGO + LTO optimized build (or baseline if --pgo none).
# bolt Stage 3: BOLT binary layout optimization.
# clean Remove build directories (profile data preserved).
#
# USAGE:
# ./build-citron-linux.sh <stage> [options]
#
# OPTIONS:
# --build DIR Build root directory (default: ./build)
# --jobs N Parallel compile jobs (default: nproc)
# --pgo ir|fe|none PGO instrumentation mode (default: ir)
# ir = LLVM IR PGO (-fprofile-generate/-fprofile-use)
# LTO mode MUST match between generate,
# csgenerate, and use stages.
# fe = Frontend PGO (-fprofile-instr-generate/-use)
# CS-IRPGO not available with fe.
# none = No PGO. Baseline build.
# --lto thin|full|none LTO mode (default: full)
# --lite-lto Alias for --lto thin
# --no-lto Alias for --lto none
# --arch x86_64|v3|aarch64|auto
# Optimization target (default: auto)
# v3 = x86-64-v3 (AVX2, BMI, FMA — Haswell+)
# --unity Enable unity (jumbo) builds (~30-90% faster compile)
# --relwithdebinfo Include debug symbols alongside optimizations
# --clang-version N Clang version to use (default: 21)
# --help, -h Show this message
#
# TYPICAL WORKFLOWS:
#
# Baseline (no PGO):
# ./build-citron-linux.sh setup
# ./build-citron-linux.sh use --pgo none --lto full
# # Binary: ./build/use-nopgo/bin/citron
#
# IR PGO (recommended):
# ./build-citron-linux.sh setup
# ./build-citron-linux.sh generate --pgo ir --lto full
# # Run ./build/generate/bin/citron, play 2-3 games for 5-10 min, exit cleanly.
# ./build-citron-linux.sh use --pgo ir --lto full
# # Binary: ./build/use/bin/citron
#
# CS-IRPGO (two profiling sessions, better inlining data):
# ./build-citron-linux.sh generate --pgo ir --lto full
# # Run ./build/generate/bin/citron, play games, exit cleanly.
# ./build-citron-linux.sh use --pgo ir --lto full
# # (produces default.profdata — also a usable binary)
# ./build-citron-linux.sh csgenerate --pgo ir --lto full
# # Run ./build/cs-generate/bin/citron, play the same games, exit cleanly.
# # cs-default-*.profraw lands next to the binary — copy to build/pgo-profiles/cs/
# ./build-citron-linux.sh use --pgo ir --lto full
# # (auto-merges CS layer → merged.profdata, rebuilds)
# # Binary: ./build/use/bin/citron
#
# IR PGO + BOLT:
# (after completing 'use' above)
# ./build-citron-linux.sh bolt --pgo ir --lto full
# # BOLT pauses — run instrumented binary, exit, press Enter.
# # Binary: ./build/bolt/citron
#
# DEPENDENCIES MANAGED BY CMAKE (not this script):
# All C++ library dependencies are fetched from source by CPM at cmake
# configure time. No system library packages are required.
# Qt is downloaded via aqt by CMakeModules/qt_download.cmake.
# FFmpeg source is downloaded via CPM; built by externals/ffmpeg/CMakeLists.txt.
# OpenSSL is built from source by CMakeModules/openssl_build.cmake.
#
# REQUIRED HOST TOOLS (installed by the setup stage):
# clang / clang++ / lld / llvm-profdata compiler toolchain
# cmake + ninja build system
# git CPM source fetches
# nasm FFmpeg assembly optimisations
# perl OpenSSL Configure script
# python3 + aqtinstall Qt binary download (invoked by cmake)
# autoconf + automake + make FFmpeg autotools build
# glslang (glslc) Vulkan shader compilation
# patchelf bundle RPATH normalization
# =============================================================================
set -euo pipefail
# =============================================================================
# Locate repo root (script lives in repo root)
# =============================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
[[ -f "${SCRIPT_DIR}/CMakeLists.txt" ]] \
|| { echo "[ERROR] CMakeLists.txt not found next to this script." >&2; exit 1; }
# =============================================================================
# Configuration defaults
# =============================================================================
CLANG_VERSION="${CLANG_VERSION:-21}"
BUILD_ROOT="${BUILD_ROOT:-${SCRIPT_DIR}/build}"
JOBS="${JOBS:-$(nproc 2>/dev/null || echo 4)}"
LTO_MODE="${LTO_MODE:-full}"
PGO_MODE="${PGO_MODE:-ir}"
UNITY_BUILD="${UNITY_BUILD:-OFF}"
BUILD_TYPE="${BUILD_TYPE:-Release}"
CPM_SOURCE_CACHE="${CPM_SOURCE_CACHE:-${HOME}/.cache/cpm}"
CPM_SOURCE_CACHE="${CPM_SOURCE_CACHE/#\~/$HOME}"
# Uncomment to optimize for this machine's CPU — produces a non-portable binary
#MARCH_NATIVE="-march=native"
MARCH_NATIVE="${MARCH_NATIVE:-}"
_ARCH_ARG="${_ARCH_ARG:-auto}"
# =============================================================================
# Derived paths (updated when --build is passed)
# =============================================================================
_set_derived_paths() {
BUILD_GENERATE="${BUILD_ROOT}/generate"
BUILD_CSGENERATE="${BUILD_ROOT}/cs-generate"
BUILD_USE="${BUILD_ROOT}/use"
BUILD_BOLT="${BUILD_ROOT}/bolt"
PROFILE_DIR="${BUILD_ROOT}/pgo-profiles"
BOLT_PROFILE_DIR="${BUILD_ROOT}/bolt-profiles"
}
_set_derived_paths
_set_clang_tools() {
CLANG="clang-${CLANG_VERSION}"
CLANGPP="clang++-${CLANG_VERSION}"
LLVM_PROFDATA="llvm-profdata-${CLANG_VERSION}"
LLVM_BOLT="llvm-bolt-${CLANG_VERSION}"
MERGE_FDATA="merge-fdata-${CLANG_VERSION}"
LLD="lld-${CLANG_VERSION}"
}
_set_clang_tools
# =============================================================================
# Colour helpers
# =============================================================================
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'
info() { echo -e "${CYAN}[INFO]${RESET} $*" >&2; }
success() { echo -e "${GREEN}[OK]${RESET} $*" >&2; }
warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
error() { echo -e "${RED}[ERROR]${RESET} $*" >&2; exit 1; }
header() {
echo -e "\n${BOLD}${GREEN}=================================================================${RESET}" >&2
echo -e "${BOLD}${GREEN} $*${RESET}" >&2
echo -e "${BOLD}${GREEN}=================================================================${RESET}" >&2
}
# =============================================================================
# Validate CPM_SOURCE_CACHE
# =============================================================================
if [[ "${CPM_SOURCE_CACHE}" == *" "* ]]; then
error "CPM_SOURCE_CACHE ('${CPM_SOURCE_CACHE}') contains spaces.\n" \
" CPM and some build tools do not support paths with spaces.\n" \
" Please set CPM_SOURCE_CACHE to a path without spaces, e.g.:\n" \
" export CPM_SOURCE_CACHE=\"/tmp/cpm-cache\""
fi
# =============================================================================
# Architecture flags
# =============================================================================
resolve_arch_flags() {
if [[ -n "${MARCH_NATIVE:-}" ]]; then
ARCH_FLAGS="${MARCH_NATIVE}"
else
local host_arch; host_arch="$(uname -m)"
case "${_ARCH_ARG}" in
v3)
[[ "${host_arch}" == "x86_64" ]] \
|| error "--arch v3 requires an x86_64 host (this machine is ${host_arch})"
ARCH_FLAGS="-march=x86-64-v3 -mtune=generic" ;;
x86_64) ARCH_FLAGS="-march=x86-64 -mtune=generic" ;;
aarch64) ARCH_FLAGS="-march=armv8-a -mtune=generic" ;;
auto|*)
case "${host_arch}" in
x86_64) ARCH_FLAGS="-march=x86-64 -mtune=generic" ;;
aarch64) ARCH_FLAGS="-march=armv8-a -mtune=generic" ;;
*) ARCH_FLAGS="" ;;
esac ;;
esac
fi
info "Architecture flags: ${ARCH_FLAGS:-(none)}"
}
# =============================================================================
# LTO helpers
# =============================================================================
lto_cmake_flag() { case "${LTO_MODE}" in full|thin) echo "ON" ;; *) echo "OFF" ;; esac; }
lto_clang_flag() { case "${LTO_MODE}" in full) echo "-flto" ;; thin) echo "-flto=thin" ;; *) echo "" ;; esac; }
# =============================================================================
# PGO flag helpers
# =============================================================================
pgo_gen_compile_flag() {
# $1 = output directory for profraw files
local output_dir="$1"
[[ "${PGO_MODE}" == "ir" ]] \
&& echo "-fprofile-generate=\"${output_dir}/default-%p.profraw\"" \
|| echo "-fprofile-instr-generate=\"${output_dir}/default-%p.profraw\""
}
pgo_use_compile_flag() {
# $1 = path to .profdata file
local profdata="$1"
[[ "${PGO_MODE}" == "ir" ]] \
&& echo "-fprofile-use=\"${profdata}\"" \
|| echo "-fprofile-instr-use=\"${profdata}\" -Wno-profile-instr-unprofiled -Wno-profile-instr-out-of-date"
}
# build_compile_flags: assemble CMAKE_C/CXX_FLAGS_<BT> for a stage.
# Args: stage (generate|csgenerate|use|nopgo) [extra]
# generate: extra = profile output directory
# csgenerate: extra = "stage1_profdata:cs_output_dir"
# use: extra = path to .profdata
# nopgo: extra = (unused)
build_compile_flags() {
local stage="$1"; local extra="${2:-}"
local debug_flag=""; [[ "${BUILD_TYPE}" == "RelWithDebInfo" ]] && debug_flag="-g"
local lto_flag; lto_flag="$(lto_clang_flag)"
local base="-O3 -DNDEBUG ${debug_flag} -USuccess -UNone ${ARCH_FLAGS:-} -Wno-error -w"
case "${stage}" in
generate)
echo "${base} $(pgo_gen_compile_flag "${extra}")${lto_flag:+ ${lto_flag}}"
;;
csgenerate)
# Use stage1 profile to guide inlining; layer CS counters on top.
local pd="${extra%%:*}"; local cs_dir="${extra##*:}"
echo "${base} $(pgo_use_compile_flag "${pd}") -fcs-profile-generate=\"${cs_dir}/cs-default-%p.profraw\"${lto_flag:+ ${lto_flag}}"
;;
use)
# LTO flag before pgo_use so profile flag reaches the LTO backend.
echo "${base} ${lto_flag:+${lto_flag} }$(pgo_use_compile_flag "${extra}")"
;;
nopgo)
echo "${base}${lto_flag:+ ${lto_flag}}"
;;
esac
}
# build_linker_flags: assemble CMAKE_EXE_LINKER_FLAGS_<BT> for a stage.
#
# Critical for IR PGO + LTO:
# -fprofile-use MUST appear on the linker command line when LTO is active.
# Without it the LTO backend's link-time code generation pass runs without
# profile guidance — negating inlining and hot-path layout decisions that
# require profile data to be meaningful at LTCG time.
#
# For the generate/csgenerate stages, we also force-keep profile runtime
# entry points that the linker might otherwise dead-strip:
# -u __llvm_profile_write_file pulls InstrProfilingFile.o (write logic)
# -u __llvm_profile_runtime pulls InstrProfilingRuntime.o whose
# constructor registers the flush-on-exit hook
build_linker_flags() {
local stage="$1"; local extra="${2:-}"
local debug_flag=""; [[ "${BUILD_TYPE}" == "RelWithDebInfo" ]] && debug_flag="-g"
local lto_flag; lto_flag="$(lto_clang_flag)"
2026-05-09 14:09:00 +12:00
local base="-fuse-ld=${LLD} -Wl,-z,nopack-relative-relocs"
case "${stage}" in
generate|csgenerate)
# Include the full compile flags so the profiling runtime is linked
# correctly, then add the force-keep symbols.
echo "${base} $(build_compile_flags "${stage}" "${extra}") -Wl,-u,__llvm_profile_write_file,-u,__llvm_profile_runtime"
;;
use)
# Pass pgo_use flag to linker so LTO backend gets profile guidance.
echo "${base} ${debug_flag:+${debug_flag} }${lto_flag:+${lto_flag} }$(pgo_use_compile_flag "${extra}")"
;;
nopgo)
echo "${base}${lto_flag:+ ${lto_flag}}"
;;
esac
}
# =============================================================================
# Distro / package manager detection
# =============================================================================
detect_pkg_manager() {
if command -v apt-get &>/dev/null; then echo "apt"
elif command -v pacman &>/dev/null; then echo "pacman"
elif command -v dnf &>/dev/null; then echo "dnf"
elif command -v yum &>/dev/null; then echo "yum"
elif command -v zypper &>/dev/null; then echo "zypper"
elif command -v emerge &>/dev/null; then echo "emerge"
else echo "unknown"
fi
}
# =============================================================================
# Stage: setup
# =============================================================================
stage_setup() {
header "Setting Up Build Environment"
local pkg_mgr; pkg_mgr="$(detect_pkg_manager)"
info "Package manager: ${pkg_mgr}"
case "${pkg_mgr}" in
apt) _setup_apt ;;
pacman) _setup_pacman ;;
dnf) _setup_dnf ;;
yum) _setup_yum ;;
zypper) _setup_zypper ;;
emerge) _setup_emerge ;;
*)
warn "Unrecognised package manager. Install manually:"
warn " clang (${CLANG_VERSION}+), lld, llvm-profdata, cmake, ninja, git,"
warn " nasm, perl, python3, python3-pip, autoconf, automake, make, glslang-tools, patchelf"
;;
esac
_install_llvm_clang
_install_aqt
_check_bolt
_verify_tools
}
_setup_apt() {
info "Updating apt package lists..."
sudo apt-get update -qq
info "Installing core build tools via apt..."
sudo apt-get install -y \
build-essential cmake ninja-build git pkg-config \
python3 python3-pip curl wget xz-utils \
nasm yasm perl \
autoconf automake make \
glslang-tools \
patchelf \
lsb-release software-properties-common gnupg \
libelf-dev libssl-dev libzstd-dev \
linux-tools-common linux-tools-generic 2>/dev/null || true
sudo apt-get install -y "linux-tools-$(uname -r)" 2>/dev/null || true
}
_setup_pacman() {
info "Installing build tools via pacman..."
sudo pacman -Syu --needed --noconfirm \
base-devel cmake ninja git \
python python-pip curl wget \
nasm yasm perl \
autoconf automake make \
glslang clang lld llvm \
patchelf perf 2>/dev/null || true
# Arch ships unversioned tools — symlink to versioned names
for tool in clang clang++ lld llvm-profdata llvm-bolt merge-fdata; do
local versioned="/usr/local/bin/${tool}-${CLANG_VERSION}"
if command -v "${tool}" &>/dev/null && [[ ! -e "${versioned}" ]]; then
sudo ln -sf "$(command -v "${tool}")" "${versioned}" 2>/dev/null || true
fi
done
success "Pacman packages installed."
}
_setup_dnf() {
info "Installing build tools via dnf..."
sudo dnf install -y \
gcc gcc-c++ cmake ninja-build git pkg-config \
python3 python3-pip curl wget xz \
nasm yasm perl \
autoconf automake make \
glslang clang lld patchelf \
elfutils-libelf-devel openssl-devel \
perf 2>/dev/null || true
sudo dnf install -y "clang${CLANG_VERSION}" "llvm${CLANG_VERSION}" 2>/dev/null \
|| warn "Versioned LLVM ${CLANG_VERSION} not in repos — using default clang."
}
_setup_yum() {
info "Installing build tools via yum..."
sudo yum install -y epel-release 2>/dev/null || true
sudo yum install -y \
gcc gcc-c++ cmake ninja-build git pkg-config \
python3 python3-pip curl wget xz \
nasm yasm perl \
autoconf automake make \
clang lld patchelf \
elfutils-libelf-devel openssl-devel \
perf 2>/dev/null || true
warn "yum/CentOS: LLVM ${CLANG_VERSION} may not be in repos. Check SCL or llvm.org."
}
_setup_zypper() {
info "Installing build tools via zypper..."
sudo zypper install -y --no-recommends \
gcc gcc-c++ cmake ninja git pkg-config \
python3 python3-pip curl wget xz \
nasm yasm perl \
autoconf automake make \
glslang clang lld llvm patchelf \
libelf-devel libopenssl-devel \
perf 2>/dev/null || true
}
_setup_emerge() {
info "Installing build tools via emerge..."
sudo emerge --ask=n \
dev-build/cmake dev-build/ninja dev-vcs/git \
dev-lang/python dev-lang/perl \
dev-lang/nasm dev-lang/yasm \
sys-devel/clang sys-devel/lld \
dev-build/autoconf dev-build/automake \
media-libs/glslang \
dev-util/patchelf \
dev-libs/elfutils dev-libs/openssl 2>/dev/null || true
}
_install_llvm_clang() {
if command -v "${CLANG}" &>/dev/null; then
success "${CLANG} already available: $(command -v "${CLANG}")"
return 0
fi
local pkg_mgr; pkg_mgr="$(detect_pkg_manager)"
if [[ "${pkg_mgr}" != "apt" ]]; then
warn "${CLANG} not found. Install LLVM ${CLANG_VERSION} manually for your distro."
return 0
fi
info "Installing LLVM/Clang ${CLANG_VERSION} from apt.llvm.org..."
wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh
chmod +x /tmp/llvm.sh
sudo /tmp/llvm.sh "${CLANG_VERSION}" all \
|| sudo /tmp/llvm.sh "${CLANG_VERSION}" \
|| error "LLVM ${CLANG_VERSION} install failed — see https://apt.llvm.org"
sudo apt-get install -y \
"clang-${CLANG_VERSION}" "clang++-${CLANG_VERSION}" \
"lld-${CLANG_VERSION}" "llvm-${CLANG_VERSION}" \
"llvm-${CLANG_VERSION}-dev" "libclang-rt-${CLANG_VERSION}-dev" \
2>/dev/null || warn "Some LLVM ${CLANG_VERSION} packages unavailable."
success "${CLANG} installed."
}
_install_aqt() {
# aqt is used by CMakeModules/qt_download.cmake to fetch Qt binaries at
# cmake configure time. It must be installed before the first cmake run.
if command -v aqt &>/dev/null || python3 -m aqt --version &>/dev/null 2>&1; then
success "aqt already available"
return 0
fi
info "Installing aqtinstall (Qt binary downloader — invoked by cmake)..."
python3 -m pip install aqtinstall --break-system-packages --quiet \
|| python3 -m pip install aqtinstall --user --quiet \
|| error "aqtinstall install failed — run: pip3 install aqtinstall"
success "aqtinstall installed."
}
_check_bolt() {
if command -v "${LLVM_BOLT}" &>/dev/null; then
success "${LLVM_BOLT} available"
return 0
fi
if command -v llvm-bolt &>/dev/null; then
sudo ln -sf "$(command -v llvm-bolt)" "/usr/local/bin/${LLVM_BOLT}" 2>/dev/null || true
sudo ln -sf "$(command -v merge-fdata)" "/usr/local/bin/${MERGE_FDATA}" 2>/dev/null || true
return 0
fi
warn "${LLVM_BOLT} not found. The bolt stage will build it from source (~15-20 min)."
}
_verify_tools() {
echo ""
info "Verifying installation..."
local ok=1
for tool in "${CLANG}" "${CLANGPP}" "${LLD}" "${LLVM_PROFDATA}" \
cmake ninja git nasm perl python3 patchelf; do
if command -v "${tool}" &>/dev/null; then
success " ${tool} -> $(command -v "${tool}")"
else
warn " ${tool} -> NOT FOUND"; ok=0
fi
done
if command -v aqt &>/dev/null || python3 -m aqt --version &>/dev/null 2>&1; then
success " aqt -> available"
else
warn " aqt -> NOT FOUND (Qt download by cmake will fail)"; ok=0
fi
[[ ${ok} -eq 1 ]] && success "All required tools available." \
|| warn "Some tools missing — check output above."
echo ""
info "Setup complete. Typical next steps:"
echo " ./build-citron-linux.sh use --pgo none --lto full # quick baseline"
echo " ./build-citron-linux.sh generate --pgo ir --lto full # start IR PGO"
}
# =============================================================================
# Source patches (run against the repo this script lives in)
# =============================================================================
apply_source_patches() {
info "Applying source compatibility patches..."
# Boost.Asio API: io_service → io_context (Boost 1.74+)
find "${SCRIPT_DIR}/src" -type f \( -name '*.cpp' -o -name '*.h' \) \
-exec sed -i \
-e 's/\bboost::asio::io_service\b/boost::asio::io_context/g' \
-e 's/\bboost::asio::io_service::strand\b/boost::asio::strand<boost::asio::io_context::executor_type>/g' \
{} + 2>/dev/null || true
# Boost.Process v1 (async_pipe moved in Boost 1.86+)
find "${SCRIPT_DIR}/src" -type f \( -name '*.cpp' -o -name '*.h' \) \
-exec sed -i \
-e 's|#include *<boost/process/async_pipe\.hpp>|#include <boost/process/v1/async_pipe.hpp>|g' \
-e 's/\bboost::process::async_pipe\b/boost::process::v1::async_pipe/g' \
{} + 2>/dev/null || true
# sse2neon reference (AArch64 only — breaks x86_64 video_core builds)
[[ -f "${SCRIPT_DIR}/src/video_core/CMakeLists.txt" ]] && \
sed -i '/sse2neon/d' "${SCRIPT_DIR}/src/video_core/CMakeLists.txt" \
2>/dev/null || true
# xbyak cmake_minimum_required floor (some forks still carry 2.8)
[[ -f "${SCRIPT_DIR}/externals/xbyak/CMakeLists.txt" ]] && \
sed -i 's/cmake_minimum_required(VERSION 2\.8)/cmake_minimum_required(VERSION 3.5)/' \
"${SCRIPT_DIR}/externals/xbyak/CMakeLists.txt" 2>/dev/null || true
success "Source patches applied."
}
# =============================================================================
# Profile helpers
# =============================================================================
normalize_profraw_dirs() {
# IR PGO on Linux writes a directory named default-<pid>.profraw/ containing
# numbered chunk files rather than a flat .profraw file. Flatten them so
# llvm-profdata can glob *.profraw directly.
local base_dir="$1"
[[ -d "${base_dir}" ]] || return 0
local entry
while IFS= read -r -d '' entry; do
[[ -d "${entry}" ]] || continue
local dir_name="${entry##*/}"
local prefix="${dir_name%.profraw}"
local idx=0 file
while IFS= read -r -d '' file; do
[[ -f "${file}" ]] || continue
local target="${base_dir}/${prefix}${idx:+-${idx}}.profraw"
while [[ -e "${target}" ]]; do
idx=$((idx + 1))
target="${base_dir}/${prefix}-${idx}.profraw"
done
mv "${file}" "${target}"; idx=$((idx + 1))
done < <(find "${entry}" -maxdepth 1 -type f -name '*.profraw' -print0)
rm -rf "${entry}"
info "Flattened profraw directory: ${dir_name}"
done < <(find "${base_dir}" -maxdepth 1 -type d -name '*.profraw' -print0)
}
_collect_appimage_profiles() {
# Profiles generated by running the AppImage are written next to the AppImage file
# by the citron.sh wrapper. We collect them back to the main profile directory
# so they can be merged and used by the build script.
# Standard PGO profiles
if [[ -d "${BUILD_GENERATE}/AppImage" ]]; then
local count; count="$(find "${BUILD_GENERATE}/AppImage" -maxdepth 1 -name "*.profraw" 2>/dev/null | wc -l)"
if [[ "${count}" -gt 0 ]]; then
info "Collecting ${count} profile(s) from ${BUILD_GENERATE}/AppImage..."
mv "${BUILD_GENERATE}/AppImage"/*.profraw "${PROFILE_DIR}/" 2>/dev/null || true
fi
fi
# CS-PGO profiles
if [[ -d "${BUILD_CSGENERATE}/AppImage" ]]; then
local count; count="$(find "${BUILD_CSGENERATE}/AppImage" -maxdepth 1 -name "*.profraw" 2>/dev/null | wc -l)"
if [[ "${count}" -gt 0 ]]; then
info "Collecting ${count} CS profile(s) from ${BUILD_CSGENERATE}/AppImage..."
mkdir -p "${PROFILE_DIR}/cs"
mv "${BUILD_CSGENERATE}/AppImage"/*.profraw "${PROFILE_DIR}/cs/" 2>/dev/null || true
fi
fi
}
_merge_profraw_to_profdata() {
local profile_dir="$1"; local output_file="$2"
normalize_profraw_dirs "${profile_dir}"
local count; count="$(find "${profile_dir}" -maxdepth 1 -name "*.profraw" 2>/dev/null | wc -l)"
[[ "${count}" -gt 0 ]] || { info "No .profraw files in ${profile_dir}."; return 1; }
info "Merging ${count} .profraw file(s) → ${output_file##*/}..."
local cmd="${LLVM_PROFDATA}"
command -v "${cmd}" &>/dev/null || cmd="llvm-profdata"
command -v "${cmd}" &>/dev/null || error "llvm-profdata not found. Run setup first."
"${cmd}" merge --sparse --output="${output_file}" "${profile_dir}"/*.profraw
success "Merged: ${output_file}"
}
resolve_use_profdata() {
_collect_appimage_profiles
# Determine which profdata to hand to the use stage.
# Preference order: merged.profdata (stage1+CS) > default.profdata (stage1) > merge from profraw.
local merged="${PROFILE_DIR}/merged.profdata"
local stage1="${PROFILE_DIR}/default.profdata"
# Invalidate merged if there are newer unmerged CS profraw files
if [[ -f "${merged}" && -d "${PROFILE_DIR}/cs" ]]; then
normalize_profraw_dirs "${PROFILE_DIR}/cs" 2>/dev/null || true
local cs_pending
cs_pending="$(find "${PROFILE_DIR}/cs" -maxdepth 1 -name "*.profraw" 2>/dev/null | wc -l)"
if [[ "${cs_pending}" -gt 0 ]]; then
warn "merged.profdata stale — ${cs_pending} unmerged CS file(s) pending. Rebuilding..."
rm -f "${merged}"
fi
fi
# Auto-merge CS profraw if present and merged doesn't exist yet
if [[ ! -f "${merged}" && -d "${PROFILE_DIR}/cs" ]]; then
normalize_profraw_dirs "${PROFILE_DIR}/cs" 2>/dev/null || true
local cs_count
cs_count="$(find "${PROFILE_DIR}/cs" -maxdepth 1 -name "*.profraw" 2>/dev/null | wc -l)"
if [[ "${cs_count}" -gt 0 ]]; then
[[ -f "${stage1}" ]] \
|| error "default.profdata missing — run generate + use first before using CS profiles."
info "CS profraw detected (${cs_count} files) — merging with stage1..."
local cs_tmp="${PROFILE_DIR}/cs-only.profdata"
local cmd="${LLVM_PROFDATA}"
command -v "${cmd}" &>/dev/null || cmd="llvm-profdata"
"${cmd}" merge --sparse --output="${cs_tmp}" "${PROFILE_DIR}/cs"/*.profraw
"${cmd}" merge --sparse --output="${merged}" "${stage1}" "${cs_tmp}"
rm -f "${cs_tmp}"
success "CS-IRPGO merged profile: ${merged}"
fi
fi
if [[ -f "${merged}" ]]; then echo "${merged}"; return 0
elif [[ -f "${stage1}" ]]; then echo "${stage1}"; return 0
fi
# Last resort: try merging from profraw
_merge_profraw_to_profdata "${PROFILE_DIR}" "${stage1}" \
|| error "No profile data found in ${PROFILE_DIR}.\nRun generate, collect profraw by playing games, then re-run use."
echo "${stage1}"
}
# =============================================================================
# Common cmake arguments (compile/linker flags are per-stage, not here)
# =============================================================================
build_common_cmake_args() {
# Populates the global _CMAKE_ARGS array. Using an array rather than echo +
# $() command substitution avoids bash word-splitting on paths that contain
# spaces (e.g. a username or project folder with a space in it).
_CMAKE_ARGS=(
"-G" "Ninja"
"-DCMAKE_BUILD_TYPE=${BUILD_TYPE}"
"-DCMAKE_C_COMPILER=${CLANG}"
"-DCMAKE_CXX_COMPILER=${CLANGPP}"
"-DCITRON_USE_CPM=ON"
"-DCPM_SOURCE_CACHE=${CPM_SOURCE_CACHE}"
"-DCITRON_USE_BUNDLED_VCPKG=OFF"
"-DCITRON_USE_BUNDLED_QT=ON"
"-DUSE_SYSTEM_QT=OFF"
"-DENABLE_QT6=ON"
"-DCITRON_USE_BUNDLED_FFMPEG=ON"
"-DBUILD_TESTING=OFF"
"-DCITRON_TESTS=OFF"
"-DCITRON_DOWNLOAD_TIME_ZONE_DATA=ON"
"-DCITRON_CHECK_SUBMODULES=OFF"
"-DCITRON_USE_LLVM_DEMANGLE=OFF"
"-DCITRON_USE_QT_MULTIMEDIA=ON"
"-DCITRON_USE_QT_WEB_ENGINE=OFF"
"-DENABLE_QT_TRANSLATION=ON"
"-DUSE_DISCORD_PRESENCE=ON"
"-DENABLE_WEB_SERVICE=ON"
"-DENABLE_OPENSSL=ON"
"-DBUNDLE_SPEEX=ON"
"-DCITRON_USE_FASTER_LD=OFF"
"-DCITRON_USE_EXTERNAL_Vulkan_HEADERS=ON"
"-DCITRON_USE_EXTERNAL_VULKAN_UTILITY_LIBRARIES=ON"
"-DCITRON_USE_AUTO_UPDATER=ON"
"-DCITRON_BUILD_TYPE=Release"
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5"
"-DCMAKE_C_FLAGS=-mtls-dialect=gnu2"
"-DCMAKE_CXX_FLAGS=-mtls-dialect=gnu2"
"-Wno-dev"
)
[[ "${UNITY_BUILD}" == "ON" ]] && _CMAKE_ARGS+=("-DENABLE_UNITY_BUILD=ON")
# Ensure the function always returns 0: a trailing [[ ]] that evaluates false
# would otherwise return exit code 1, triggering set -e in the caller.
:
}
print_profiling_instructions() {
local binary="$1"
echo ""
echo -e "${YELLOW}════════════════════════════════════════════════════════════════${RESET}"
echo -e "${YELLOW} Training guide for best PGO results${RESET}"
echo -e "${YELLOW}════════════════════════════════════════════════════════════════${RESET}"
echo ""
echo -e " ${BOLD}Instrumented binary:${RESET} ${binary}"
echo -e " ${BOLD}Profile output:${RESET} ${PROFILE_DIR}/"
echo ""
echo " 1. Run the instrumented binary and play 2-3 games for 5-10 min each."
echo " Navigate menus and the game list (profiles UI code too)."
echo " Exit cleanly via File > Exit or Ctrl+Q."
if [[ "${PGO_MODE}" == "ir" ]]; then
echo ""
echo " NOTE (IR PGO): LLVM writes a directory named default-<pid>.profraw/"
echo " containing numbered chunks. This is normal — the script flattens"
echo " them automatically before merging."
fi
echo ""
echo " 2. Build the optimized binary:"
echo " ./build-citron-linux.sh use --pgo ${PGO_MODE} --lto ${LTO_MODE}"
echo ""
if [[ "${PGO_MODE}" == "ir" ]]; then
echo " Optional: add CS-IRPGO (second profiling session, better inlining data):"
echo " ./build-citron-linux.sh use --pgo ir --lto ${LTO_MODE}"
echo " ./build-citron-linux.sh csgenerate --pgo ir --lto ${LTO_MODE}"
echo " # Run csgenerate binary, exit cleanly, copy cs-default-*.profraw"
echo " # to build/pgo-profiles/cs/"
echo " ./build-citron-linux.sh use --pgo ir --lto ${LTO_MODE}"
echo ""
fi
echo -e "${YELLOW}════════════════════════════════════════════════════════════════${RESET}"
echo ""
}
# =============================================================================
# Sentinel helpers
# =============================================================================
write_gen_sentinel() {
printf "LTO=%s\nPGO=%s\n" "${LTO_MODE}" "${PGO_MODE}" \
> "${BUILD_ROOT}/.citron-gen-config"
}
check_stage_compatibility() {
local gen_cfg="${BUILD_ROOT}/.citron-gen-config"
[[ -f "${gen_cfg}" ]] || return 0
local gen_lto gen_pgo
gen_lto="$(grep -oP '(?<=LTO=)\S+' "${gen_cfg}" 2>/dev/null || true)"
gen_pgo="$(grep -oP '(?<=PGO=)\S+' "${gen_cfg}" 2>/dev/null || true)"
if [[ -n "${gen_lto}" && "${gen_lto}" != "${LTO_MODE}" ]]; then
error "LTO mismatch: generate used LTO=${gen_lto}, this stage has LTO=${LTO_MODE}.\n" \
" For IR PGO, LTO must match across all stages. Use --lto ${gen_lto}"
fi
if [[ -n "${gen_pgo}" && "${gen_pgo}" != "${PGO_MODE}" ]]; then
error "PGO mode mismatch: generate used PGO=${gen_pgo}. Use --pgo ${gen_pgo}"
fi
}
# Internal: fix RPATH of binaries to find custom libs (Qt, ICU, XCB)
# =============================================================================
_patch_binary_rpaths() {
local bin_dir="$1"
local build_dir; build_dir="$(dirname "${bin_dir}")"
local config_file="${build_dir}/AppImageBuilder/config.sh"
if [[ ! -f "${config_file}" ]]; then
warn "config.sh not found in ${build_dir}, skipping RPATH patching."
return
fi
# Extract paths from the generated config.sh
local qt_path; qt_path="$(grep -oP '(?<=export CITRON_QT_PATH=")[^"]+' "${config_file}" || true)"
local icu_path; icu_path="$(grep -oP '(?<=export CITRON_ICU_PATH=")[^"]+' "${config_file}" || true)"
local xcb_path; xcb_path="$(grep -oP '(?<=export CITRON_XCB_PATH=")[^"]+' "${config_file}" || true)"
# Construct RPATH (Qt lib, ICU lib, XCB lib)
local rpath=""
[[ -n "${qt_path}" ]] && rpath="${qt_path}/lib"
[[ -n "${icu_path}" ]] && rpath="${rpath}${rpath:+:}${icu_path}/lib"
[[ -n "${xcb_path}" ]] && rpath="${rpath}${rpath:+:}${xcb_path}/lib"
if [[ -z "${rpath}" ]]; then
warn "No custom library paths found in config.sh, skipping RPATH patching."
return
fi
info "Patching RPATH for binaries in ${bin_dir}..."
for bin in "citron" "citron-cmd" "citron-room"; do
if [[ -f "${bin_dir}/${bin}" ]]; then
# We prepend the custom RPATH to any existing one
local existing_rpath; existing_rpath="$(patchelf --print-rpath "${bin_dir}/${bin}" 2>/dev/null || echo "")"
if [[ -z "${existing_rpath}" ]]; then
patchelf --set-rpath "${rpath}" "${bin_dir}/${bin}"
elif [[ "${existing_rpath}" != *"${rpath}"* ]]; then
patchelf --set-rpath "${rpath}:${existing_rpath}" "${bin_dir}/${bin}"
fi
fi
done
}
# =============================================================================
# Internal: configure + build in a given directory
# =============================================================================
_build_with_flags() {
local build_dir="$1"; local stage="$2"
shift 2; local extra_args=("$@")
local bt_upper; bt_upper="$(echo "${BUILD_TYPE}" | tr '[:lower:]' '[:upper:]')"
# nopgo is a convenience shorthand — fill in the extra_args here
if [[ "${stage}" == "nopgo" && ${#extra_args[@]} -eq 0 ]]; then
extra_args=(
"-DCITRON_ENABLE_PGO_GENERATE=OFF"
"-DCITRON_ENABLE_PGO_USE=OFF"
"-DCITRON_PGO_FLAGS_MANAGED_BY_SCRIPT=ON"
"-DCITRON_ENABLE_LTO=$(lto_cmake_flag)"
"-DCMAKE_C_FLAGS_${bt_upper}=$(build_compile_flags nopgo)"
"-DCMAKE_CXX_FLAGS_${bt_upper}=$(build_compile_flags nopgo)"
"-DCMAKE_EXE_LINKER_FLAGS_${bt_upper}=$(build_linker_flags nopgo)"
)
fi
mkdir -p "${build_dir}"; cd "${build_dir}"
rm -f CMakeCache.txt; rm -rf CMakeFiles
build_common_cmake_args
cmake "${SCRIPT_DIR}" \
"${_CMAKE_ARGS[@]}" \
"${extra_args[@]}" \
|| error "CMake configure failed"
info "Building citron (${BUILD_TYPE}, ${JOBS} jobs)..."
cmake --build . --config "${BUILD_TYPE}" -j "${JOBS}" \
|| error "cmake --build failed"
# Patch binaries to find our custom libs (Qt via aqt, ICU/XCB via /tmp)
_patch_binary_rpaths "${PWD}/bin"
}
# =============================================================================
# AppImage building
# =============================================================================
build_appimage_stage() {
local build_dir="$1"
local stage_name="$2"
local binary_override="${3:-}"
header "Building AppImage for ${stage_name}"
if [[ -n "${binary_override}" ]]; then
# Override CITRON_BINARY_DIR so build-v2.sh finds the correct binary
export CITRON_BINARY_DIR="$(dirname "${binary_override}")"
else
unset CITRON_BINARY_DIR
fi
# Run the AppImage build script
bash "${SCRIPT_DIR}/AppImageBuilder/build-v2.sh" "${build_dir}"
# Create the output directory and move the AppImage
mkdir -p "${build_dir}/AppImage/user"
mv "${build_dir}/citron-x86_64.AppImage" "${build_dir}/AppImage/"
success "AppImage for ${stage_name} placed in ${build_dir}/AppImage/citron-x86_64.AppImage"
}
# =============================================================================
# Stage: generate
# =============================================================================
stage_generate() {
header "Stage 1: PGO Instrumented Build (${PGO_MODE} PGO, LTO=${LTO_MODE})"
command -v "${CLANG}" &>/dev/null || error "${CLANG} not found. Run setup first."
command -v cmake &>/dev/null || error "cmake not found."
command -v ninja &>/dev/null || error "ninja not found."
apply_source_patches
mkdir -p "${BUILD_GENERATE}" "${PROFILE_DIR}"
# Remove stale profraw from a previous generate run so they don't
# contaminate a fresh profdata merge.
local stale
stale="$(find "${PROFILE_DIR}" -maxdepth 1 \
\( -name "*.profraw" -o \( -type d -name "*.profraw" \) \) \
2>/dev/null | wc -l)"
if [[ "${stale}" -gt 0 ]]; then
info "Removing ${stale} stale profraw entries from previous generate run..."
find "${PROFILE_DIR}" -maxdepth 1 -name "*.profraw" -delete
find "${PROFILE_DIR}" -maxdepth 1 -type d -name "*.profraw" \
-exec rm -rf {} + 2>/dev/null || true
fi
local bt_upper; bt_upper="$(echo "${BUILD_TYPE}" | tr '[:lower:]' '[:upper:]')"
local compile_flags; compile_flags="$(build_compile_flags generate "${PROFILE_DIR}")"
local linker_flags; linker_flags="$(build_linker_flags generate "${PROFILE_DIR}")"
info "Compile flags: ${compile_flags}"
info "Linker flags: ${linker_flags}"
_build_with_flags "${BUILD_GENERATE}" generate \
"-DCITRON_ENABLE_PGO_GENERATE=ON" \
"-DCITRON_ENABLE_PGO_USE=OFF" \
"-DCITRON_PGO_FLAGS_MANAGED_BY_SCRIPT=ON" \
"-DCITRON_ENABLE_LTO=$(lto_cmake_flag)" \
"-DCITRON_PGO_PROFILE_DIR=${PROFILE_DIR}" \
"-DCMAKE_C_FLAGS_${bt_upper}=${compile_flags}" \
"-DCMAKE_CXX_FLAGS_${bt_upper}=${compile_flags}" \
"-DCMAKE_EXE_LINKER_FLAGS_${bt_upper}=${linker_flags}"
write_gen_sentinel
mkdir -p "${BUILD_GENERATE}/bin/user"
success "Instrumented build: ${BUILD_GENERATE}/bin/citron"
build_appimage_stage "${BUILD_GENERATE}" "generate"
print_profiling_instructions "${BUILD_GENERATE}/bin/citron"
}
# =============================================================================
# Stage: csgenerate
#
# CRITICAL INVARIANT:
# Uses default.profdata (stage1 only) as -fprofile-use input.
# NEVER uses merged.profdata. If merged.profdata (which contains CS records
# from a prior CS cycle) were used, the inlining decisions at this stage
# would diverge from the plain stage1 baseline that the use-stage binary is
# built from — causing CS counter hash mismatches at the use stage and
# producing a worse binary than plain IR PGO.
# =============================================================================
stage_csgenerate() {
header "Stage 1b: CS-IRPGO Instrumented Build"
[[ "${PGO_MODE}" == "ir" ]] \
|| error "csgenerate requires --pgo ir. CS-IRPGO is not available for FE PGO."
command -v "${CLANG}" &>/dev/null || error "${CLANG} not found. Run setup first."
local gen_cfg="${BUILD_ROOT}/.citron-gen-config"
[[ -f "${gen_cfg}" ]] \
|| error "Generate sentinel not found at ${BUILD_ROOT}/.citron-gen-config.\nRun the generate stage first."
check_stage_compatibility
# Locate stage1 profdata — MUST be default.profdata, never merged.profdata
local stage1_pd="${PROFILE_DIR}/default.profdata"
if [[ ! -f "${stage1_pd}" ]]; then
normalize_profraw_dirs "${PROFILE_DIR}"
local count
count="$(find "${PROFILE_DIR}" -maxdepth 1 -name "*.profraw" 2>/dev/null | wc -l)"
if [[ "${count}" -gt 0 ]]; then
_merge_profraw_to_profdata "${PROFILE_DIR}" "${stage1_pd}"
elif [[ -f "${PROFILE_DIR}/merged.profdata" ]]; then
error "default.profdata missing but merged.profdata exists.\n" \
" merged.profdata contains CS records and CANNOT be used as the\n" \
" csgenerate baseline — see script header for explanation.\n" \
" Fix: re-run 'use --pgo ir --lto ${LTO_MODE}' to produce a\n" \
" fresh default.profdata, then re-run csgenerate."
else
error "No stage1 profdata or profraw found in ${PROFILE_DIR}/.\n" \
" Run generate, collect profraw by playing games, then:\n" \
" ./build-citron-linux.sh use --pgo ir --lto ${LTO_MODE}\n" \
" (produces default.profdata), then re-run csgenerate."
fi
fi
info "Stage1 profdata (plain IR, no CS): ${stage1_pd}"
local cs_dir="${PROFILE_DIR}/cs"
mkdir -p "${BUILD_CSGENERATE}" "${cs_dir}"
local bt_upper; bt_upper="$(echo "${BUILD_TYPE}" | tr '[:lower:]' '[:upper:]')"
local compile_flags; compile_flags="$(build_compile_flags csgenerate "${stage1_pd}:${cs_dir}")"
local linker_flags; linker_flags="$(build_linker_flags csgenerate "${stage1_pd}:${cs_dir}")"
info "Compile flags: ${compile_flags}"
info "Linker flags: ${linker_flags}"
apply_source_patches
_build_with_flags "${BUILD_CSGENERATE}" csgenerate \
"-DCITRON_ENABLE_PGO_GENERATE=ON" \
"-DCITRON_ENABLE_PGO_USE=OFF" \
"-DCITRON_PGO_FLAGS_MANAGED_BY_SCRIPT=ON" \
"-DCITRON_ENABLE_LTO=$(lto_cmake_flag)" \
"-DCITRON_PGO_PROFILE_DIR=${cs_dir}" \
"-DCMAKE_C_FLAGS_${bt_upper}=${compile_flags}" \
"-DCMAKE_CXX_FLAGS_${bt_upper}=${compile_flags}" \
"-DCMAKE_EXE_LINKER_FLAGS_${bt_upper}=${linker_flags}"
mkdir -p "${BUILD_CSGENERATE}/bin/user"
success "CS-IRPGO instrumented build: ${BUILD_CSGENERATE}/bin/citron"
build_appimage_stage "${BUILD_CSGENERATE}" "csgenerate"
echo ""
echo -e "${YELLOW}════════════════════════════════════════════════════════════════${RESET}"
echo -e "${YELLOW} NEXT STEP: Collect CS Profile Data (Session 2)${RESET}"
echo -e "${YELLOW}════════════════════════════════════════════════════════════════${RESET}"
echo ""
echo " Run: ${BUILD_CSGENERATE}/bin/citron"
echo " Play the same games as session 1 for 15-30 min. Exit cleanly."
echo " cs-default-<pid>.profraw (or directory) lands next to the binary."
echo " Copy it to: ${cs_dir}/"
echo ""
echo " Then rebuild — the use stage merges stage1 + CS automatically:"
echo " ./build-citron-linux.sh use --pgo ir --lto ${LTO_MODE}"
echo ""
echo -e "${YELLOW}════════════════════════════════════════════════════════════════${RESET}"
echo ""
}
# =============================================================================
# Stage: merge
# =============================================================================
stage_merge() {
header "Merging PGO Profiles"
_collect_appimage_profiles
[[ -d "${PROFILE_DIR}" ]] || error "Profile directory not found: ${PROFILE_DIR}"
_merge_profraw_to_profdata "${PROFILE_DIR}" "${PROFILE_DIR}/default.profdata"
find "${PROFILE_DIR}" -maxdepth 1 -name "*.profraw" -delete
info "Run 'summary' to check coverage, or 'use' to build the optimized binary."
}
# =============================================================================
# Stage: summary
# =============================================================================
stage_summary() {
header "Profile Summary"
[[ -d "${PROFILE_DIR}" ]] || { warn "Profile directory not found: ${PROFILE_DIR}"; return 0; }
local cmd="${LLVM_PROFDATA}"
command -v "${cmd}" &>/dev/null || cmd="llvm-profdata"
for pd in "${PROFILE_DIR}/merged.profdata" "${PROFILE_DIR}/default.profdata"; do
if [[ -f "${pd}" ]]; then
local sz; sz="$(du -h "${pd}" | cut -f1)"
success "Profile: ${pd} (${sz})"
command -v "${cmd}" &>/dev/null && \
"${cmd}" show --counts --all-functions "${pd}" 2>/dev/null | tail -6 || true
fi
done
local pr; pr="$(find "${PROFILE_DIR}" -maxdepth 1 -name "*.profraw" 2>/dev/null | wc -l)"
[[ "${pr}" -gt 0 ]] && warn "${pr} unmerged .profraw file(s). Run: merge"
local cs; cs="$(find "${PROFILE_DIR}/cs" -name "*.profraw" 2>/dev/null | wc -l)"
[[ "${cs}" -gt 0 ]] && \
info "${cs} CS profraw file(s) in pgo-profiles/cs/ (auto-merged on next 'use')"
[[ ! -f "${PROFILE_DIR}/default.profdata" && "${pr}" -eq 0 ]] && \
warn "No profile data found in ${PROFILE_DIR}"
}
# =============================================================================
# Stage: use
# =============================================================================
stage_use() {
if [[ "${PGO_MODE}" == "none" ]]; then
header "Build: Baseline Release (no PGO, LTO=${LTO_MODE})"
apply_source_patches
_build_with_flags "${BUILD_ROOT}/use-nopgo" nopgo
mkdir -p "${BUILD_ROOT}/use-nopgo/bin/user"
success "Baseline build: ${BUILD_ROOT}/use-nopgo/bin/citron"
build_appimage_stage "${BUILD_ROOT}/use-nopgo" "use-nopgo"
return 0
fi
header "Stage 2: PGO + LTO Optimized Build"
command -v "${CLANG}" &>/dev/null || error "${CLANG} not found. Run setup first."
check_stage_compatibility
local profdata; profdata="$(resolve_use_profdata)"
[[ "${profdata}" == "${PROFILE_DIR}/merged.profdata" ]] \
&& info "Using CS-IRPGO merged profile: ${profdata}" \
|| info "Using stage1 profile: ${profdata}"
apply_source_patches
local bt_upper; bt_upper="$(echo "${BUILD_TYPE}" | tr '[:lower:]' '[:upper:]')"
local compile_flags; compile_flags="$(build_compile_flags use "${profdata}")"
local linker_flags; linker_flags="$(build_linker_flags use "${profdata}")"
info "Compile flags: ${compile_flags}"
info "Linker flags: ${linker_flags}"
info " ↳ -fprofile-use on linker enables LTO backend LTCG with profile guidance"
_build_with_flags "${BUILD_USE}" use \
"-DCITRON_ENABLE_PGO_USE=ON" \
"-DCITRON_ENABLE_PGO_GENERATE=OFF" \
"-DCITRON_PGO_FLAGS_MANAGED_BY_SCRIPT=ON" \
"-DCITRON_ENABLE_LTO=$(lto_cmake_flag)" \
"-DCITRON_PGO_PROFILE_DIR=${PROFILE_DIR}" \
"-DCMAKE_C_FLAGS_${bt_upper}=${compile_flags}" \
"-DCMAKE_CXX_FLAGS_${bt_upper}=${compile_flags}" \
"-DCMAKE_EXE_LINKER_FLAGS_${bt_upper}=${linker_flags}"
local pgo_label
[[ "${profdata}" == "${PROFILE_DIR}/merged.profdata" ]] \
&& pgo_label="CS-IRPGO (stage1 IR + CS layer)" \
|| pgo_label="IR PGO (stage1 only)"
echo ""
success "════════════════════════════════════════════════════════════════"
success " Stage use complete"
mkdir -p "${BUILD_USE}/bin/user"
success " Binary: ${BUILD_USE}/bin/citron"
success " PGO: ${pgo_label}"
success " LTO: ${LTO_MODE}${LTO_MODE:+ ($(lto_clang_flag))}"
success "════════════════════════════════════════════════════════════════"
build_appimage_stage "${BUILD_USE}" "use"
echo ""
info "Next steps (optional):"
echo " BOLT: ./build-citron-linux.sh bolt --pgo ${PGO_MODE} --lto ${LTO_MODE}"
if [[ "${profdata}" != "${PROFILE_DIR}/merged.profdata" && "${PGO_MODE}" == "ir" ]]; then
echo ""
echo " CS-IRPGO (add a second profiling layer):"
echo " ./build-citron-linux.sh csgenerate --pgo ir --lto ${LTO_MODE}"
fi
echo ""
}
# =============================================================================
# BOLT: build from source if needed
# =============================================================================
_ensure_bolt() {
if command -v "${LLVM_BOLT}" &>/dev/null && command -v "${MERGE_FDATA}" &>/dev/null; then
success "${LLVM_BOLT} available"; return 0
fi
if command -v llvm-bolt &>/dev/null; then
sudo ln -sf "$(command -v llvm-bolt)" "/usr/local/bin/${LLVM_BOLT}" 2>/dev/null || true
sudo ln -sf "$(command -v merge-fdata)" "/usr/local/bin/${MERGE_FDATA}" 2>/dev/null || true
return 0
fi
header "Building LLVM BOLT ${CLANG_VERSION} from Source (~15-20 min)"
local bolt_src="/tmp/llvm-bolt-${CLANG_VERSION}-src"
local bolt_build="/tmp/llvm-bolt-${CLANG_VERSION}-build"
local found_tag=""
for minor in 0.0 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9; do
local candidate="llvmorg-${CLANG_VERSION}.${minor}"
if git ls-remote --tags https://github.com/llvm/llvm-project.git \
"${candidate}" 2>/dev/null | grep -q "${candidate}"; then
found_tag="${candidate}"
fi
done
[[ -n "${found_tag}" ]] || error "No LLVM ${CLANG_VERSION} release tag found on GitHub."
info "Using LLVM tag: ${found_tag}"
if [[ ! -d "${bolt_src}/.git" ]]; then
git clone --depth=1 --branch "${found_tag}" \
--filter=blob:none --sparse \
https://github.com/llvm/llvm-project.git "${bolt_src}"
pushd "${bolt_src}" > /dev/null
git sparse-checkout set llvm bolt cmake third-party
popd > /dev/null
fi
cmake -S "${bolt_src}/llvm" -B "${bolt_build}" -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DLLVM_ENABLE_PROJECTS="bolt" \
-DLLVM_TARGETS_TO_BUILD="X86;AArch64" \
-DLLVM_INCLUDE_TESTS=OFF -DLLVM_INCLUDE_EXAMPLES=OFF \
-DCMAKE_C_COMPILER="${CLANG}" -DCMAKE_CXX_COMPILER="${CLANGPP}"
cmake --build "${bolt_build}" --target llvm-bolt merge-fdata bolt_rt -j "${JOBS}"
sudo cp "${bolt_build}/bin/llvm-bolt" "/usr/local/bin/${LLVM_BOLT}"
sudo cp "${bolt_build}/bin/merge-fdata" "/usr/local/bin/${MERGE_FDATA}"
sudo chmod +x "/usr/local/bin/${LLVM_BOLT}" "/usr/local/bin/${MERGE_FDATA}"
sudo cp "${bolt_build}/lib/libbolt_rt_instr.a" /usr/local/lib/ 2>/dev/null || true
sudo cp "${bolt_build}/lib/libbolt_rt_hugify.a" /usr/local/lib/ 2>/dev/null || true
success "${LLVM_BOLT} installed"
}
# =============================================================================
# Stage: bolt
# =============================================================================
stage_bolt() {
header "Stage 3A: BOLT Binary Layout Optimization"
_ensure_bolt
# BOLT requires the ELF to have been linked with --emit-relocs.
# We build a dedicated relocs-enabled binary rather than relying on the
# use-stage binary (which typically is not built with --emit-relocs).
local relocs_dir="${BUILD_ROOT}/use-relocs"
if [[ ! -f "${relocs_dir}/bin/citron" ]]; then
info "Building use-stage binary with --emit-relocs for BOLT..."
apply_source_patches
local bt_upper; bt_upper="$(echo "${BUILD_TYPE}" | tr '[:lower:]' '[:upper:]')"
if [[ "${PGO_MODE}" != "none" ]]; then
check_stage_compatibility
local profdata; profdata="$(resolve_use_profdata)"
local compile_flags; compile_flags="$(build_compile_flags use "${profdata}")"
# Append --emit-relocs to the use-stage linker flags
local linker_flags; linker_flags="$(build_linker_flags use "${profdata}") -Wl,--emit-relocs"
_build_with_flags "${relocs_dir}" use \
"-DCITRON_ENABLE_PGO_USE=ON" \
"-DCITRON_ENABLE_PGO_GENERATE=OFF" \
"-DCITRON_PGO_FLAGS_MANAGED_BY_SCRIPT=ON" \
"-DCITRON_ENABLE_LTO=$(lto_cmake_flag)" \
"-DCITRON_PGO_PROFILE_DIR=${PROFILE_DIR}" \
"-DCMAKE_C_FLAGS_${bt_upper}=${compile_flags}" \
"-DCMAKE_CXX_FLAGS_${bt_upper}=${compile_flags}" \
"-DCMAKE_EXE_LINKER_FLAGS_${bt_upper}=${linker_flags}"
else
local compile_flags; compile_flags="$(build_compile_flags nopgo)"
local linker_flags; linker_flags="$(build_linker_flags nopgo) -Wl,--emit-relocs"
_build_with_flags "${relocs_dir}" use \
"-DCITRON_ENABLE_PGO_USE=OFF" \
"-DCITRON_ENABLE_PGO_GENERATE=OFF" \
"-DCITRON_PGO_FLAGS_MANAGED_BY_SCRIPT=ON" \
"-DCITRON_ENABLE_LTO=$(lto_cmake_flag)" \
"-DCMAKE_C_FLAGS_${bt_upper}=${compile_flags}" \
"-DCMAKE_CXX_FLAGS_${bt_upper}=${compile_flags}" \
"-DCMAKE_EXE_LINKER_FLAGS_${bt_upper}=${linker_flags}"
fi
success "use-with-relocs build: ${relocs_dir}/bin/citron"
fi
local elf_binary="${relocs_dir}/bin/citron"
mkdir -p "${BOLT_PROFILE_DIR}" "${BUILD_BOLT}/bin"
local instrumented="${BUILD_BOLT}/bin/citron-bolt-instrumented"
local fdata_pattern="${BOLT_PROFILE_DIR}/citron-%p.fdata"
local merged_fdata="${BOLT_PROFILE_DIR}/citron-merged.fdata"
local skipped_funcs="RC4_options"
info "Instrumenting ELF with BOLT..."
"${LLVM_BOLT}" "${elf_binary}" \
--instrument \
--skip-funcs="${skipped_funcs}" \
--instrumentation-file="${fdata_pattern}" \
--instrumentation-file-append-pid \
-o "${instrumented}"
success "Instrumented: ${instrumented}"
# Load custom library paths from config.sh for the run instructions
local lp_env=""
local config_file="${relocs_dir}/AppImageBuilder/config.sh"
if [[ -f "${config_file}" ]]; then
# Use a subshell to avoid polluting our current environment
lp_env="$( (source "${config_file}"; echo "LD_LIBRARY_PATH=\"${CITRON_QT_PATH}/lib:${CITRON_ICU_PATH}:${CITRON_XCB_PATH}/lib\" ") )"
fi
echo ""
echo -e "${YELLOW}════════════════════════════════════════════════════════════════${RESET}"
echo -e "${YELLOW} Run the BOLT-instrumented binary${RESET}"
echo -e "${YELLOW}════════════════════════════════════════════════════════════════${RESET}"
echo ""
echo " Run the instrumented binary with custom library paths:"
echo " ${lp_env}${instrumented}"
echo ""
echo " Play for 15-30 min. Exit cleanly."
echo " fdata files will appear in: ${BOLT_PROFILE_DIR}/"
echo ""
echo -e "${YELLOW}════════════════════════════════════════════════════════════════${RESET}"
read -rp " Press Enter once you have exited the instrumented binary... "
echo ""
local fdata_count
fdata_count="$(find "${BOLT_PROFILE_DIR}" -name "*.fdata" 2>/dev/null | wc -l)"
[[ "${fdata_count}" -gt 0 ]] \
|| error "No .fdata files found in ${BOLT_PROFILE_DIR}. Run the instrumented binary first."
info "Merging ${fdata_count} .fdata file(s)..."
"${MERGE_FDATA}" "${BOLT_PROFILE_DIR}"/*.fdata -o "${merged_fdata}"
success "Merged: ${merged_fdata}"
info "Optimizing ELF with BOLT..."
"${LLVM_BOLT}" "${elf_binary}" \
-p "${merged_fdata}" \
--skip-funcs="${skipped_funcs}" \
--reorder-blocks=ext-tsp \
--reorder-functions=cdsort \
--split-functions \
--split-all-cold \
--split-eh \
--dyno-stats \
-o "${BUILD_BOLT}/bin/citron" \
|| error "BOLT optimization failed."
echo ""
success "════════════════════════════════════════════════════════════════"
success " Stage bolt complete"
mkdir -p "${BUILD_BOLT}/user"
success " Binary: ${BUILD_BOLT}/bin/citron"
success " Optimizations: PGO + LTO + BOLT basic-block reordering"
success "════════════════════════════════════════════════════════════════"
# Patch the final BOLT binary too
_patch_binary_rpaths "${BUILD_BOLT}/bin"
build_appimage_stage "${relocs_dir}" "bolt" "${BUILD_BOLT}/bin/citron"
echo ""
}
# =============================================================================
# Stage: clean
# =============================================================================
stage_clean() {
header "Cleaning Build Directories"
local backup="/tmp/citron-pgo-backup-$$"
[[ -d "${PROFILE_DIR}" ]] && { info "Backing up profile data..."; cp -r "${PROFILE_DIR}" "${backup}"; }
[[ -d "${BUILD_ROOT}" ]] && { info "Removing ${BUILD_ROOT}..."; rm -rf "${BUILD_ROOT}"; }
if [[ -d "${backup}" ]]; then
mkdir -p "${PROFILE_DIR}"
cp -r "${backup}/." "${PROFILE_DIR}/"
rm -rf "${backup}"
success "Profile data preserved: ${PROFILE_DIR}"
fi
success "Clean complete."
}
# =============================================================================
# Argument parsing
# =============================================================================
STAGE=""
while [[ $# -gt 0 ]]; do
case "$1" in
setup|generate|csgenerate|merge|summary|use|bolt|clean)
STAGE="$1"; shift ;;
--build)
BUILD_ROOT="$2"; _set_derived_paths; shift 2 ;;
--jobs|-j)
JOBS="$2"; shift 2 ;;
--pgo-type|--pgo)
case "$2" in
ir|fe|none) PGO_MODE="$2"; shift 2 ;;
*) error "--pgo requires: ir, fe, or none" ;;
esac ;;
--lto)
case "$2" in
thin|full|none) LTO_MODE="$2"; shift 2 ;;
*) error "--lto requires: thin, full, or none" ;;
esac ;;
--lite-lto) LTO_MODE="thin"; shift ;;
--no-lto) LTO_MODE="none"; shift ;;
--arch)
case "$2" in
x86_64|v3|aarch64|auto) _ARCH_ARG="$2"; shift 2 ;;
*) error "--arch requires: x86_64, v3, aarch64, or auto" ;;
esac ;;
--unity) UNITY_BUILD="ON"; shift ;;
--no-unity) UNITY_BUILD="OFF"; shift ;;
--relwithdebinfo) BUILD_TYPE="RelWithDebInfo"; shift ;;
--clang-version)
CLANG_VERSION="$2"; _set_clang_tools; shift 2 ;;
--help|-h)
sed -n '/^# build-citron-linux/,/^# ===/p' "$0" | head -130
exit 0 ;;
*) error "Unknown argument: $1\nRun with --help for usage." ;;
esac
done
[[ -n "${STAGE}" ]] \
|| error "No stage specified.\nUsage: $0 <stage> [options]\nStages: setup generate csgenerate merge summary use bolt clean"
resolve_arch_flags
case "${STAGE}" in
setup) stage_setup ;;
generate) stage_generate ;;
csgenerate) stage_csgenerate ;;
merge) stage_merge ;;
summary) stage_summary ;;
use) stage_use ;;
bolt) stage_bolt ;;
clean) stage_clean ;;
esac