updated the AppImage builder and attached it to the build script

More dependency handling fixes
This commit is contained in:
cheezwiz7899
2026-05-08 17:14:34 +12:00
committed by Ploo
parent 8cddd542b6
commit a731faa39d
12 changed files with 471 additions and 402 deletions
+1 -1
View File
@@ -2,5 +2,5 @@
Type=Application
Name=citron
Icon=citron
Exec=AppRun
Exec=citron
Categories=Game;
+23 -2
View File
@@ -1,3 +1,24 @@
#! /bin/sh
#!/bin/sh
# citron AppImage wrapper script
LD_LIBRARY_PATH=/usr/lib/$(uname -m)-linux-gnu:/usr/lib:. exec ./citron "$@"
# Find the directory where the AppImage is located
if [ -n "$APPIMAGE" ]; then
APPIMAGE_DIR="$(dirname "$APPIMAGE")"
else
# Fallback for non-AppImage execution
APPIMAGE_DIR="$(cd "$(dirname "$0")" && pwd)"
fi
# Set PGO profile output to be next to the AppImage
# This ensures that instrumented builds write their results where the user expects.
if [ -z "$LLVM_PROFILE_FILE" ]; then
export LLVM_PROFILE_FILE="$APPIMAGE_DIR/default-%p.profraw"
fi
# We don't 'cd' to APPIMAGE_DIR to avoid breaking relative paths for games,
# but Citron will find the 'user' folder if it's in the current working directory
# from which the AppImage was launched.
# Run the real binary
# $APPDIR is the mount point set by the AppRun loader
exec "$APPDIR/usr/bin/citron.bin" "$@"
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 12 KiB

+102
View File
@@ -0,0 +1,102 @@
#!/bin/bash
set -e
# Get script directory
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
BUILD_DIR="${1:-$SCRIPT_DIR/../build/generate}"
# Save environment overrides
ENV_QT_PATH="${CITRON_QT_PATH:-}"
ENV_ICU_PATH="${CITRON_ICU_PATH:-}"
ENV_XCB_PATH="${CITRON_XCB_PATH:-}"
ENV_BINARY_DIR="${CITRON_BINARY_DIR:-}"
if [ ! -f "$BUILD_DIR/AppImageBuilder/config.sh" ]; then
echo "Error: config.sh not found in $BUILD_DIR/AppImageBuilder/"
echo "Please run CMake first."
exit 1
fi
source "$BUILD_DIR/AppImageBuilder/config.sh"
# Allow environment variables to override config.sh
export CITRON_QT_PATH="${ENV_QT_PATH:-$CITRON_QT_PATH}"
export CITRON_ICU_PATH="${ENV_ICU_PATH:-$CITRON_ICU_PATH}"
export CITRON_XCB_PATH="${ENV_XCB_PATH:-$CITRON_XCB_PATH}"
export CITRON_BINARY_DIR="${ENV_BINARY_DIR:-$CITRON_BINARY_DIR}"
# Tools setup
TOOLS_DIR="$BUILD_DIR/AppImageBuilder/tools"
mkdir -p "$TOOLS_DIR"
LINUXDEPLOY="$TOOLS_DIR/linuxdeploy-x86_64.AppImage"
LINUXDEPLOY_QT="$TOOLS_DIR/linuxdeploy-plugin-qt-x86_64.AppImage"
download_tool() {
local url=$1
local file=$2
if [ ! -f "$file" ]; then
echo "Downloading $file..."
wget -q --show-progress "$url" -O "$file"
chmod +x "$file"
fi
}
download_tool "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage" "$LINUXDEPLOY"
download_tool "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage" "$LINUXDEPLOY_QT"
# AppDir setup
APPDIR="$BUILD_DIR/AppImageBuilder/AppDir"
rm -rf "$APPDIR"
mkdir -p "$APPDIR"
# Build the AppImage
# We need to tell linuxdeploy where our custom libraries are
export LD_LIBRARY_PATH="$CITRON_ICU_PATH:$CITRON_XCB_PATH/lib:$LD_LIBRARY_PATH"
export EXTRA_QT_PLUGINS="wayland" # Add any extra plugins needed
# Run linuxdeploy
# --plugin qt handles Qt dependencies
export QMAKE="$CITRON_QT_PATH/bin/qmake"
export QT_ROOT="$CITRON_QT_PATH"
export LD_LIBRARY_PATH="$CITRON_QT_PATH/lib:$CITRON_ICU_PATH:$CITRON_XCB_PATH/lib:${LD_LIBRARY_PATH:-}"
pushd "$BUILD_DIR" > /dev/null
# Run linuxdeploy to populate AppDir
$LINUXDEPLOY --appdir "$APPDIR" \
--executable "$CITRON_BINARY_DIR/citron" \
--desktop-file "$SCRIPT_DIR/assets/citron.desktop" \
--icon-file "$SCRIPT_DIR/assets/citron.svg" \
--library "$CITRON_ICU_PATH/libicuuc.so.73" \
--library "$CITRON_ICU_PATH/libicui18n.so.73" \
--library "$CITRON_ICU_PATH/libicudata.so.73" \
--library "$CITRON_XCB_PATH/lib/libxcb.so.1" \
--library "$CITRON_XCB_PATH/lib/libxcb-cursor.so.0" \
--library "$CITRON_XCB_PATH/lib/libxcb-xkb.so.1" \
--library "$CITRON_XCB_PATH/lib/libxcb-render.so.0" \
--library "$CITRON_XCB_PATH/lib/libxcb-render-util.so.0" \
--library "$CITRON_XCB_PATH/lib/libxcb-shape.so.0" \
--library "$CITRON_XCB_PATH/lib/libxcb-shm.so.0" \
--library "$CITRON_XCB_PATH/lib/libxcb-sync.so.1" \
--library "$CITRON_XCB_PATH/lib/libxcb-xfixes.so.0" \
--library "$CITRON_XCB_PATH/lib/libxcb-randr.so.0" \
--library "$CITRON_XCB_PATH/lib/libxcb-image.so.0" \
--library "$CITRON_XCB_PATH/lib/libxcb-keysyms.so.1" \
--library "$CITRON_XCB_PATH/lib/libxcb-icccm.so.4" \
--library "$CITRON_XCB_PATH/lib/libXau.so.6" \
--library "$CITRON_XCB_PATH/lib/libXdmcp.so.6" \
--plugin qt
# Inject the wrapper script to support portable mode (user folder) and PGO profiles
# We rename the real binary to citron.bin and put the script as 'citron'
if [ -f "$APPDIR/usr/bin/citron" ]; then
mv "$APPDIR/usr/bin/citron" "$APPDIR/usr/bin/citron.bin"
cp "$SCRIPT_DIR/assets/citron.sh" "$APPDIR/usr/bin/citron"
chmod +x "$APPDIR/usr/bin/citron"
fi
# Build the final AppImage
$LINUXDEPLOY --appdir "$APPDIR" --output appimage
popd > /dev/null
echo "AppImage build complete in $BUILD_DIR"
+6
View File
@@ -0,0 +1,6 @@
# Build configuration for AppImageBuilder
export CITRON_QT_PATH="@QT_TARGET_PATH@"
export CITRON_ICU_PATH="@ICU_BINARY_DIR@"
export CITRON_XCB_PATH="@XCB_BUILD_ROOT@"
export CITRON_BINARY_DIR="@CMAKE_BINARY_DIR@/bin"
export ARCH="@CMAKE_SYSTEM_PROCESSOR@"
+22 -20
View File
@@ -582,23 +582,23 @@ if (ENABLE_QT AND NOT CITRON_USE_CPM)
# Try to find Qt6 installation path (aqtinstall layout: externals/qt/<version>/<arch>)
# We prioritize llvm-mingw_64 for Clang builds to ensure libc++ compatibility.
if (EXISTS "${CMAKE_BINARY_DIR}/externals/qt/6.9.3/llvm-mingw_64")
set(QT_TARGET_PATH "${CMAKE_BINARY_DIR}/externals/qt/6.9.3/llvm-mingw_64")
set(QT_TARGET_PATH "${CMAKE_BINARY_DIR}/externals/qt/6.9.3/llvm-mingw_64" CACHE PATH "Path to Qt6" FORCE)
elseif (EXISTS "${CMAKE_BINARY_DIR}/externals/qt/6.9.3/mingw_64")
set(QT_TARGET_PATH "${CMAKE_BINARY_DIR}/externals/qt/6.9.3/mingw_64")
set(QT_TARGET_PATH "${CMAKE_BINARY_DIR}/externals/qt/6.9.3/mingw_64" CACHE PATH "Path to Qt6" FORCE)
elseif (EXISTS "${CMAKE_BINARY_DIR}/externals/qt/6.9.3/msvc2022_64")
set(QT_TARGET_PATH "${CMAKE_BINARY_DIR}/externals/qt/6.9.3/msvc2022_64")
set(QT_TARGET_PATH "${CMAKE_BINARY_DIR}/externals/qt/6.9.3/msvc2022_64" CACHE PATH "Path to Qt6" FORCE)
elseif (EXISTS "${CMAKE_BINARY_DIR}/externals/qt/6.9.3/msvc2022_arm64")
set(QT_TARGET_PATH "${CMAKE_BINARY_DIR}/externals/qt/6.9.3/msvc2022_arm64")
set(QT_TARGET_PATH "${CMAKE_BINARY_DIR}/externals/qt/6.9.3/msvc2022_arm64" CACHE PATH "Path to Qt6" FORCE)
elseif (EXISTS "${CMAKE_BINARY_DIR}/externals/qt/6.9.3/msvc2019_64")
set(QT_TARGET_PATH "${CMAKE_BINARY_DIR}/externals/qt/6.9.3/msvc2019_64")
set(QT_TARGET_PATH "${CMAKE_BINARY_DIR}/externals/qt/6.9.3/msvc2019_64" CACHE PATH "Path to Qt6" FORCE)
elseif (EXISTS "${CMAKE_BINARY_DIR}/externals/qt/6.8.3/llvm-mingw_64")
set(QT_TARGET_PATH "${CMAKE_BINARY_DIR}/externals/qt/6.8.3/llvm-mingw_64")
set(QT_TARGET_PATH "${CMAKE_BINARY_DIR}/externals/qt/6.8.3/llvm-mingw_64" CACHE PATH "Path to Qt6" FORCE)
elseif (EXISTS "${CMAKE_BINARY_DIR}/externals/qt/6.8.3/msvc2022_64")
set(QT_TARGET_PATH "${CMAKE_BINARY_DIR}/externals/qt/6.8.3/msvc2022_64")
set(QT_TARGET_PATH "${CMAKE_BINARY_DIR}/externals/qt/6.8.3/msvc2022_64" CACHE PATH "Path to Qt6" FORCE)
elseif (EXISTS "${CMAKE_BINARY_DIR}/externals/qt/6.8.3/msvc2019_64")
set(QT_TARGET_PATH "${CMAKE_BINARY_DIR}/externals/qt/6.8.3/msvc2019_64")
set(QT_TARGET_PATH "${CMAKE_BINARY_DIR}/externals/qt/6.8.3/msvc2019_64" CACHE PATH "Path to Qt6" FORCE)
elseif (EXISTS "${CMAKE_BINARY_DIR}/externals/qt/6.7.3/msvc2019_64")
set(QT_TARGET_PATH "${CMAKE_BINARY_DIR}/externals/qt/6.7.3/msvc2019_64")
set(QT_TARGET_PATH "${CMAKE_BINARY_DIR}/externals/qt/6.7.3/msvc2019_64" CACHE PATH "Path to Qt6" FORCE)
endif()
endif()
@@ -626,18 +626,12 @@ if (ENABLE_QT AND NOT CITRON_USE_CPM)
endif()
# Fallback detection if QT_TARGET_PATH was not found in common bundled locations
if (NOT QT_TARGET_PATH)
get_target_property(qtcore_path Qt6::Core LOCATION_Release)
if (qtcore_path)
string(FIND "${qtcore_path}" "/bin/" qtcore_path_bin_pos REVERSE)
string(FIND "${qtcore_path}" "/lib/" qtcore_path_lib_pos REVERSE)
if (qtcore_path_bin_pos GREATER qtcore_path_lib_pos)
string(SUBSTRING "${qtcore_path}" 0 ${qtcore_path_bin_pos} QT_TARGET_PATH)
else()
string(SUBSTRING "${qtcore_path}" 0 ${qtcore_path_lib_pos} QT_TARGET_PATH)
endif()
endif()
if (NOT QT_TARGET_PATH AND Qt6_DIR)
get_filename_component(_tmp_path "${Qt6_DIR}/../../.." ABSOLUTE)
set(QT_TARGET_PATH "${_tmp_path}" CACHE PATH "Path to Qt6" FORCE)
message(STATUS "Resolved QT_TARGET_PATH from Qt6_DIR: ${QT_TARGET_PATH}")
endif()
message(STATUS "Final QT_TARGET_PATH: ${QT_TARGET_PATH}")
endif()
function(set_citron_qt_components)
@@ -974,3 +968,11 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND CITRON_ENABLE_LTO)
endif()
endforeach()
endif()
# Generate AppImage build configuration
if (UNIX AND NOT APPLE)
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/AppImageBuilder/config.sh.in"
"${CMAKE_CURRENT_BINARY_DIR}/AppImageBuilder/config.sh"
@ONLY
)
endif()
-152
View File
@@ -1,152 +0,0 @@
# SPDX-FileCopyrightText: 2026 citron Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
if (NOT DEFINED BUNDLE_BIN_DIR)
message(FATAL_ERROR "BUNDLE_BIN_DIR is required")
endif()
if (NOT DEFINED PATCHELF_EXE OR NOT EXISTS "${PATCHELF_EXE}")
message(FATAL_ERROR "patchelf is required to make the Linux bundle portable")
endif()
set(_bin_dir "${BUNDLE_BIN_DIR}")
set(_lib_dir "${_bin_dir}/lib")
set(_plugins_dir "${_bin_dir}/plugins")
file(MAKE_DIRECTORY "${_lib_dir}")
if (DEFINED RUNTIME_DEPENDENCY_DIRS)
string(REPLACE "|" ";" _runtime_dependency_dirs "${RUNTIME_DEPENDENCY_DIRS}")
endif()
list(APPEND _runtime_dependency_dirs "${_lib_dir}")
list(REMOVE_DUPLICATES _runtime_dependency_dirs)
foreach(_candidate citron citron-cmd citron-room shader_tool)
if (EXISTS "${_bin_dir}/${_candidate}")
list(APPEND _executables "${_bin_dir}/${_candidate}")
endif()
endforeach()
file(GLOB _bundled_libraries "${_lib_dir}/*.so" "${_lib_dir}/*.so.*")
if (EXISTS "${_plugins_dir}")
file(GLOB_RECURSE _bundled_modules "${_plugins_dir}/*.so")
endif()
if (_executables OR _bundled_libraries OR _bundled_modules)
file(GET_RUNTIME_DEPENDENCIES
RESOLVED_DEPENDENCIES_VAR _resolved_dependencies
UNRESOLVED_DEPENDENCIES_VAR _unresolved_dependencies
CONFLICTING_DEPENDENCIES_PREFIX _conflicting
EXECUTABLES ${_executables}
LIBRARIES ${_bundled_libraries}
MODULES ${_bundled_modules}
DIRECTORIES ${_runtime_dependency_dirs}
PRE_EXCLUDE_REGEXES
[[^libgcc_s\.so\..*]]
[[^libstdc\+\+\.so\..*]]
[[^libglapi\.so\..*]]
[[^libGL\.so\..*]]
[[^libEGL\.so\..*]]
[[^libvulkan\.so\..*]]
[[^libXau\.so\..*]]
[[^libXdmcp\.so\..*]]
[[^libxcb\.so\..*]]
POST_EXCLUDE_REGEXES
[[/ld-linux[^/]*\.so(\..*)?$]]
[[/libc\.so(\..*)?$]]
[[/libm\.so(\..*)?$]]
[[/libdl\.so(\..*)?$]]
[[/libpthread\.so(\..*)?$]]
[[/librt\.so(\..*)?$]]
[[/libresolv\.so(\..*)?$]]
[[/libutil\.so(\..*)?$]]
[[/libnss_[^/]*\.so(\..*)?$]]
[[/libanl\.so(\..*)?$]]
[[/libgcc_s\.so(\..*)?$]]
[[/libstdc\+\+\.so(\..*)?$]]
)
endif()
foreach(_dependency ${_resolved_dependencies})
get_filename_component(_dependency_name "${_dependency}" NAME)
if (NOT EXISTS "${_lib_dir}/${_dependency_name}")
file(COPY "${_dependency}" DESTINATION "${_lib_dir}" FOLLOW_SYMLINK_CHAIN)
endif()
endforeach()
if (_unresolved_dependencies)
list(REMOVE_DUPLICATES _unresolved_dependencies)
message(WARNING "Unresolved Linux bundle dependencies: ${_unresolved_dependencies}")
endif()
function(_citron_is_elf output_var file_path)
execute_process(
COMMAND readelf -h "${file_path}"
RESULT_VARIABLE _readelf_result
OUTPUT_QUIET
ERROR_QUIET
)
if (_readelf_result EQUAL 0)
set(${output_var} TRUE PARENT_SCOPE)
else()
set(${output_var} FALSE PARENT_SCOPE)
endif()
endfunction()
function(_citron_set_rpath file_path rpath)
_citron_is_elf(_is_elf "${file_path}")
if (NOT _is_elf)
return()
endif()
execute_process(
COMMAND "${PATCHELF_EXE}" --force-rpath --set-rpath "${rpath}" "${file_path}"
RESULT_VARIABLE _patchelf_result
OUTPUT_VARIABLE _patchelf_stdout
ERROR_VARIABLE _patchelf_stderr
)
if (NOT _patchelf_result EQUAL 0)
message(FATAL_ERROR
"Failed to set RPATH on ${file_path}\n"
"${_patchelf_stdout}\n${_patchelf_stderr}")
endif()
endfunction()
foreach(_executable ${_executables})
_citron_set_rpath("${_executable}" "$ORIGIN/lib")
endforeach()
file(GLOB _final_libraries "${_lib_dir}/*.so" "${_lib_dir}/*.so.*")
foreach(_library ${_final_libraries})
_citron_set_rpath("${_library}" "$ORIGIN")
endforeach()
if (EXISTS "${_plugins_dir}")
file(GLOB_RECURSE _final_modules "${_plugins_dir}/*.so")
foreach(_module ${_final_modules})
_citron_set_rpath("${_module}" "$ORIGIN/../../lib")
endforeach()
endif()
set(_rpath_failures)
foreach(_file ${_executables} ${_final_libraries} ${_final_modules})
_citron_is_elf(_is_elf "${_file}")
if (NOT _is_elf)
continue()
endif()
execute_process(
COMMAND readelf -d "${_file}"
OUTPUT_VARIABLE _dynamic_section
ERROR_QUIET
)
if (_dynamic_section MATCHES "(RPATH|RUNPATH)[^\n]*\\[/" OR
_dynamic_section MATCHES "(RPATH|RUNPATH)[^\n]*:/")
list(APPEND _rpath_failures "${_file}")
endif()
endforeach()
if (_rpath_failures)
list(REMOVE_DUPLICATES _rpath_failures)
message(FATAL_ERROR "Absolute RPATH/RUNPATH remains in Linux bundle: ${_rpath_failures}")
endif()
message(STATUS "Linux bundle dependencies copied and RPATHs normalized in ${_bin_dir}")
+1
View File
@@ -591,6 +591,7 @@ set(CITRON_ICU_TAG "release-73-2" CACHE STRING "ICU git tag/version")
set(CITRON_XCB_MACROS_VER "1.20.2" CACHE STRING "XCB util-macros version")
set(CITRON_XCB_PROTO_VER "1.17.0" CACHE STRING "XCB proto version")
set(CITRON_XCB_XAU_VER "1.0.11" CACHE STRING "XCB libXau version")
set(CITRON_XCB_XDMCP_VER "1.1.5" CACHE STRING "XCB libXdmcp version")
set(CITRON_XCB_LIBXCB_VER "1.16" CACHE STRING "XCB libxcb version")
set(CITRON_XCB_UTIL_VER "0.4.1" CACHE STRING "XCB util version")
set(CITRON_XCB_CURSOR_VER "0.1.6" CACHE STRING "XCB cursor version")
+6
View File
@@ -70,6 +70,10 @@ endif()
# ── Download target Qt ────────────────────────────────────────────────────────
if (Qt6_DIR AND EXISTS "${Qt6_DIR}/Qt6Config.cmake")
message(STATUS "[Qt] Using target Qt from Qt6_DIR: ${Qt6_DIR}")
if (NOT QT_TARGET_PATH)
get_filename_component(_tmp_path "${Qt6_DIR}/../../.." ABSOLUTE)
set(QT_TARGET_PATH "${_tmp_path}" CACHE PATH "Path to Qt6 target root" FORCE)
endif()
else()
set(_QT_TARGET_DIR "${CITRON_QT_BASE_DIR}/${CITRON_QT_VERSION}/${_QT_DIR_NAME}")
set(_QT_TARGET_CMAKE "${_QT_TARGET_DIR}/${_QT_CMAKE_SUB}/Qt6Config.cmake")
@@ -118,7 +122,9 @@ else()
if (EXISTS "${_QT_TARGET_CMAKE}")
get_filename_component(_qt6_dir "${_QT_TARGET_CMAKE}" DIRECTORY)
set(Qt6_DIR "${_qt6_dir}" CACHE PATH "Path to Qt6Config.cmake (from aqt)" FORCE)
set(QT_TARGET_PATH "${_QT_TARGET_DIR}" CACHE PATH "Path to Qt6 target root" FORCE)
message(STATUS "[Qt] Qt6_DIR = ${Qt6_DIR}")
message(STATUS "[Qt] QT_TARGET_PATH = ${QT_TARGET_PATH}")
endif()
endif()
+5 -1
View File
@@ -10,7 +10,7 @@ endif()
# We need a list of components to fetch and build.
# 'macros' and 'proto' must come first as others depend on them.
set(_xcb_components macros proto xau libxcb util image keysyms renderutil wm cursor)
set(_xcb_components macros proto xau xdmcp libxcb util image keysyms renderutil wm cursor)
set(XCB_BUILD_ROOT "${CMAKE_BINARY_DIR}/externals/xcb-build")
file(MAKE_DIRECTORY "${XCB_BUILD_ROOT}")
@@ -21,6 +21,7 @@ set(XCB_BINARY_DIR "${XCB_BUILD_ROOT}/lib" CACHE INTERNAL "Location of CPM-built
set(_xcb_macros_ver "${CITRON_XCB_MACROS_VER}")
set(_xcb_proto_ver "${CITRON_XCB_PROTO_VER}")
set(_xcb_xau_ver "${CITRON_XCB_XAU_VER}")
set(_xcb_xdmcp_ver "${CITRON_XCB_XDMCP_VER}")
set(_xcb_libxcb_ver "${CITRON_XCB_LIBXCB_VER}")
set(_xcb_cursor_ver "${CITRON_XCB_CURSOR_VER}")
set(_xcb_util_ver "${CITRON_XCB_UTIL_VER}")
@@ -38,6 +39,9 @@ foreach(_comp ${_xcb_components})
elseif(_comp STREQUAL "xau")
set(_url "https://xorg.freedesktop.org/archive/individual/lib/libXau-${_xcb_xau_ver}.tar.xz")
set(_name "libXau")
elseif(_comp STREQUAL "xdmcp")
set(_url "https://xorg.freedesktop.org/archive/individual/lib/libXdmcp-${_xcb_xdmcp_ver}.tar.xz")
set(_name "libXdmcp")
elseif(_comp STREQUAL "libxcb")
set(_url "https://xcb.freedesktop.org/dist/libxcb-${_xcb_libxcb_ver}.tar.xz")
set(_name "libxcb")
+83 -192
View File
@@ -26,7 +26,7 @@
# - 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/propeller stages):
# 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.
@@ -47,8 +47,7 @@
# 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 3A: BOLT binary layout optimization.
# propeller Stage 3B: Propeller BB+function layout (requires perf + LBR).
# bolt Stage 3: BOLT binary layout optimization.
# clean Remove build directories (profile data preserved).
#
# USAGE:
@@ -107,12 +106,6 @@
# # BOLT pauses — run instrumented binary, exit, press Enter.
# # Binary: ./build/bolt/citron
#
# IR PGO + Propeller (requires perf + LBR-capable CPU):
# (after completing 'use' above)
# ./build-citron-linux.sh propeller --pgo ir --lto full
# # Propeller pauses — run perf command shown, exit, press Enter.
# # Binary: ./build/propeller/bin/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.
@@ -130,7 +123,6 @@
# autoconf + automake + make FFmpeg autotools build
# glslang (glslc) Vulkan shader compilation
# patchelf bundle RPATH normalization
# perf propeller stage only
# =============================================================================
set -euo pipefail
@@ -167,10 +159,8 @@ _set_derived_paths() {
BUILD_CSGENERATE="${BUILD_ROOT}/cs-generate"
BUILD_USE="${BUILD_ROOT}/use"
BUILD_BOLT="${BUILD_ROOT}/bolt"
BUILD_PROPELLER="${BUILD_ROOT}/propeller"
PROFILE_DIR="${BUILD_ROOT}/pgo-profiles"
BOLT_PROFILE_DIR="${BUILD_ROOT}/bolt-profiles"
PROPELLER_PROFILE_DIR="${BUILD_ROOT}/propeller-profiles"
}
_set_derived_paths
@@ -191,14 +181,14 @@ _set_clang_tools
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} $*"; }
success() { echo -e "${GREEN}[OK]${RESET} $*"; }
warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; }
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}"
echo -e "${BOLD}${GREEN} $*${RESET}"
echo -e "${BOLD}${GREEN}=================================================================${RESET}"
echo -e "\n${BOLD}${GREEN}=================================================================${RESET}" >&2
echo -e "${BOLD}${GREEN} $*${RESET}" >&2
echo -e "${BOLD}${GREEN}=================================================================${RESET}" >&2
}
# =============================================================================
@@ -590,6 +580,31 @@ normalize_profraw_dirs() {
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}"
@@ -604,6 +619,7 @@ _merge_profraw_to_profdata() {
}
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"
@@ -783,6 +799,33 @@ _build_with_flags() {
cmake --build . --config "${BUILD_TYPE}" -j "${JOBS}"
}
# =============================================================================
# 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
# =============================================================================
@@ -829,6 +872,7 @@ stage_generate() {
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"
}
@@ -902,6 +946,7 @@ stage_csgenerate() {
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}"
@@ -925,6 +970,7 @@ stage_csgenerate() {
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
@@ -972,6 +1018,7 @@ stage_use() {
_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
@@ -1017,10 +1064,10 @@ stage_use() {
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}"
echo " Propeller: ./build-citron-linux.sh propeller --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):"
@@ -1137,20 +1184,32 @@ stage_bolt() {
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 " ${instrumented}"
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}/"
@@ -1170,6 +1229,7 @@ stage_bolt() {
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 \
@@ -1186,178 +1246,10 @@ stage_bolt() {
success " Binary: ${BUILD_BOLT}/bin/citron"
success " Optimizations: PGO + LTO + BOLT basic-block reordering"
success "════════════════════════════════════════════════════════════════"
build_appimage_stage "${relocs_dir}" "bolt" "${BUILD_BOLT}/bin/citron"
echo ""
}
# =============================================================================
# Propeller helpers
# =============================================================================
_ensure_create_llvm_prof() {
local ver_sentinel="/usr/local/bin/.create_llvm_prof_ver"
local clang_ver; clang_ver="$("${CLANG}" --version 2>&1 | head -1 || echo unknown)"
if command -v create_llvm_prof &>/dev/null; then
local stored=""; [[ -f "${ver_sentinel}" ]] && stored="$(cat "${ver_sentinel}")"
if [[ "${clang_ver}" == "${stored}" ]]; then
success "create_llvm_prof available"; return 0
fi
warn "create_llvm_prof version mismatch — rebuilding."
sudo rm -f /usr/local/bin/create_llvm_prof "${ver_sentinel}"
fi
header "Building create_llvm_prof (google/llvm-propeller)"
local pkg_mgr; pkg_mgr="$(detect_pkg_manager)"
case "${pkg_mgr}" in
apt) sudo apt-get install -y libelf-dev libssl-dev libzstd-dev 2>/dev/null || true ;;
pacman) sudo pacman -S --needed --noconfirm elfutils openssl zstd 2>/dev/null || true ;;
dnf) sudo dnf install -y elfutils-libelf-devel openssl-devel libzstd-devel 2>/dev/null || true ;;
zypper) sudo zypper install -y libelf-devel libopenssl-devel libzstd-devel 2>/dev/null || true ;;
esac
local src="/tmp/propeller-src" bld="/tmp/propeller-build"
[[ -d "${src}/.git" ]] \
|| git clone --depth=1 https://github.com/google/llvm-propeller.git "${src}"
rm -rf "${bld}"
CC="${CLANG}" CXX="${CLANGPP}" \
cmake -S "${src}" -B "${bld}" -G Ninja -DCMAKE_BUILD_TYPE=Release \
|| error "llvm-propeller cmake configure failed."
cmake --build "${bld}" --target generate_propeller_profiles -j "${JOBS}" \
|| error "llvm-propeller build failed."
sudo cp "${bld}/propeller/generate_propeller_profiles" /usr/local/bin/create_llvm_prof
sudo chmod +x /usr/local/bin/create_llvm_prof
printf '%s' "${clang_ver}" | sudo tee "${ver_sentinel}" > /dev/null
success "create_llvm_prof installed."
}
# =============================================================================
# Stage: propeller
# =============================================================================
stage_propeller() {
header "Stage 3B: Propeller BB+Function Layout Optimization"
command -v perf &>/dev/null \
|| error "perf not found. Install linux-tools-$(uname -r) and re-run."
_ensure_create_llvm_prof
# Build a binary with -fbasic-block-address-map for perf profiling.
# LTO is intentionally OFF: the ThinLTO backend in lld does not propagate
# -fbasic-block-address-map through the link step, so .llvm_bb_addr_map
# sections would be absent in the final binary. PGO data alone provides
# sufficient representative coverage for the Propeller conversion step.
local bb_dir="${BUILD_ROOT}/use-bb"
if [[ ! -f "${bb_dir}/bin/citron" ]]; then
info "Building BB-annotated binary (-fbasic-block-address-map, no LTO)..."
apply_source_patches
local bt_upper; bt_upper="$(echo "${BUILD_TYPE}" | tr '[:lower:]' '[:upper:]')"
local debug_flag=""; [[ "${BUILD_TYPE}" == "RelWithDebInfo" ]] && debug_flag="-g"
local pgo_part="" pgo_enable="OFF"
if [[ "${PGO_MODE}" != "none" ]]; then
check_stage_compatibility
local profdata; profdata="$(resolve_use_profdata)"
pgo_part="$(pgo_use_compile_flag "${profdata}")"
pgo_enable="ON"
fi
local bb_cflags="-O3 -DNDEBUG ${debug_flag} -USuccess -UNone ${ARCH_FLAGS:-} -Wno-error -w ${pgo_part} -fbasic-block-address-map"
local bb_linker="-fuse-ld=${LLD} -Wl,--emit-relocs"
_build_with_flags "${bb_dir}" use \
"-DCITRON_ENABLE_PGO_USE=${pgo_enable}" \
"-DCITRON_ENABLE_PGO_GENERATE=OFF" \
"-DCITRON_PGO_FLAGS_MANAGED_BY_SCRIPT=ON" \
"-DCITRON_ENABLE_LTO=OFF" \
"-DCITRON_PGO_PROFILE_DIR=${PROFILE_DIR}" \
"-DCMAKE_C_FLAGS_${bt_upper}=${bb_cflags}" \
"-DCMAKE_CXX_FLAGS_${bt_upper}=${bb_cflags}" \
"-DCMAKE_EXE_LINKER_FLAGS_${bt_upper}=${bb_linker}"
success "BB-annotated binary: ${bb_dir}/bin/citron"
fi
local elf_binary="${bb_dir}/bin/citron"
mkdir -p "${PROPELLER_PROFILE_DIR}" "${BUILD_PROPELLER}/bin"
local perf_data="${PROPELLER_PROFILE_DIR}/perf.data"
local cc_profile="${PROPELLER_PROFILE_DIR}/propeller_cc.prof"
local symorder="${PROPELLER_PROFILE_DIR}/propeller_symorder.txt"
# Ensure perf can record branch stacks
local paranoid; paranoid="$(cat /proc/sys/kernel/perf_event_paranoid 2>/dev/null || echo 1)"
if [[ "${paranoid}" -gt 1 ]]; then
warn "perf_event_paranoid=${paranoid} — branch stacks require <= 1. Setting now..."
sudo sysctl -w kernel.perf_event_paranoid=1 \
|| error "Could not set perf_event_paranoid. Run: sudo sysctl kernel.perf_event_paranoid=1"
fi
if [[ ! -f "${perf_data}" ]]; then
echo ""
echo -e "${YELLOW}═══════════════════════════════════════════════════════════════${RESET}"
echo -e "${YELLOW} Propeller — Branch Profile Collection${RESET}"
echo -e "${YELLOW}═══════════════════════════════════════════════════════════════${RESET}"
echo ""
echo " Run the following to collect a branch-stack profile:"
echo ""
echo " perf record -b -e cycles:u \\"
echo " -o ${perf_data} \\"
echo " -- ${elf_binary}"
echo ""
echo " Play for 15-30 minutes. Exit citron cleanly."
echo ""
echo -e "${YELLOW}═══════════════════════════════════════════════════════════════${RESET}"
read -rp " Press Enter once perf has finished and perf.data is written... "
echo ""
[[ -f "${perf_data}" ]] \
|| error "perf.data not found at ${perf_data}. Run the perf command above first."
fi
info "Converting perf branch data to Propeller profiles..."
create_llvm_prof \
--binary="${elf_binary}" \
--profile="${perf_data}" \
--cc_profile="${cc_profile}" \
--ld_profile="${symorder}" \
2>&1 || error "generate_propeller_profiles failed — check perf.data and ELF compatibility."
[[ -f "${cc_profile}" ]] && success "CC profile: ${cc_profile} ($(wc -l < "${cc_profile}") entries)"
[[ -f "${symorder}" ]] && success "LD profile: ${symorder} ($(wc -l < "${symorder}") functions)"
# Final rebuild: PGO + LTO + Propeller
info "Rebuilding optimized binary with Propeller profiles (PGO + LTO + Propeller)..."
apply_source_patches
local bt_upper; bt_upper="$(echo "${BUILD_TYPE}" | tr '[:lower:]' '[:upper:]')"
local debug_flag=""; [[ "${BUILD_TYPE}" == "RelWithDebInfo" ]] && debug_flag="-g"
local lto_flag; lto_flag="$(lto_clang_flag)"
local pgo_part="" pgo_enable="OFF"
if [[ "${PGO_MODE}" != "none" ]]; then
check_stage_compatibility
local profdata; profdata="$(resolve_use_profdata)"
pgo_part="$(pgo_use_compile_flag "${profdata}")"
pgo_enable="ON"
fi
local prop_cflags="-O3 -DNDEBUG ${debug_flag} -USuccess -UNone ${ARCH_FLAGS:-} -Wno-error -w ${lto_flag:+${lto_flag} }${pgo_part}"
[[ -f "${cc_profile}" ]] && prop_cflags="${prop_cflags} -fbasic-block-sections=list=${cc_profile}"
# -fprofile-use on the linker so LTO backend gets profile guidance
local prop_linker="-fuse-ld=${LLD} ${debug_flag:+${debug_flag} }${lto_flag:+${lto_flag} }${pgo_part}"
[[ -f "${symorder}" ]] && prop_linker="${prop_linker} -Wl,--symbol-ordering-file=${symorder}"
_build_with_flags "${BUILD_PROPELLER}" use \
"-DCITRON_ENABLE_PGO_USE=${pgo_enable}" \
"-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}=${prop_cflags}" \
"-DCMAKE_CXX_FLAGS_${bt_upper}=${prop_cflags}" \
"-DCMAKE_EXE_LINKER_FLAGS_${bt_upper}=${prop_linker}"
echo ""
success "════════════════════════════════════════════════════════════════"
success " Stage propeller complete"
mkdir -p "${BUILD_PROPELLER}/bin/user"
success " Binary: ${BUILD_PROPELLER}/bin/citron"
success " Optimizations: PGO + LTO + Propeller BB+function layout"
success "════════════════════════════════════════════════════════════════"
echo ""
}
# =============================================================================
# Stage: clean
@@ -1385,7 +1277,7 @@ STAGE=""
while [[ $# -gt 0 ]]; do
case "$1" in
setup|generate|csgenerate|merge|summary|use|bolt|propeller|clean)
setup|generate|csgenerate|merge|summary|use|bolt|clean)
STAGE="$1"; shift ;;
--build)
BUILD_ROOT="$2"; _set_derived_paths; shift 2 ;;
@@ -1421,7 +1313,7 @@ while [[ $# -gt 0 ]]; do
done
[[ -n "${STAGE}" ]] \
|| error "No stage specified.\nUsage: $0 <stage> [options]\nStages: setup generate csgenerate merge summary use bolt propeller clean"
|| error "No stage specified.\nUsage: $0 <stage> [options]\nStages: setup generate csgenerate merge summary use bolt clean"
resolve_arch_flags
@@ -1433,6 +1325,5 @@ case "${STAGE}" in
summary) stage_summary ;;
use) stage_use ;;
bolt) stage_bolt ;;
propeller) stage_propeller ;;
clean) stage_clean ;;
esac
+2 -19
View File
@@ -11,18 +11,7 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON)
set_property(GLOBAL PROPERTY JOB_POOLS link_pool=1)
set(CMAKE_JOB_POOL_LINK link_pool)
# Set the RPATH for Qt Libraries
# This must be done before the `citron` target is created
if (CITRON_USE_BUNDLED_QT AND (${CMAKE_SYSTEM_NAME} STREQUAL "Linux"))
# Use relative RPATH so the binary finds libraries in the 'lib' subfolder.
# $ORIGIN refers to the directory containing the executable.
# We use --disable-new-dtags to force RPATH instead of RUNPATH,
# ensuring recursive dependency resolution for bundled libraries.
set(CMAKE_BUILD_RPATH "$ORIGIN/lib/")
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib/")
add_link_options("-Wl,--disable-new-dtags")
endif()
# citron target creation
add_executable(citron
Info.plist
about_dialog.cpp
@@ -656,14 +645,8 @@ if (WIN32 AND QT_VERSION VERSION_GREATER_EQUAL 6)
)
endif()
if (CITRON_USE_BUNDLED_QT)
if (WIN32 AND CITRON_USE_BUNDLED_QT)
include(CopyCitronQt6Deps)
if (TARGET icu-build)
add_dependencies(citron icu-build)
endif()
if (CITRON_XCB_TARGETS)
add_dependencies(citron ${CITRON_XCB_TARGETS})
endif()
copy_citron_Qt6_deps(citron)
endif()