diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000..410625c3ab --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,435 @@ +# GitLab CI config for Citron - DISABLED +# This CI configuration has been disabled as GitHub Actions is now being used +# To re-enable, uncomment the configuration below + +# stages: +# - build +# - test +# - package + +# variables: +# BUILD_TYPE: "Release" +# CMAKE_BUILD_PARALLEL_LEVEL: "4" +# DOCKER_DRIVER: overlay2 +# DOCKER_TLS_CERTDIR: "" + +# # Cache configuration for faster builds +# cache: +# paths: +# - build/vcpkg_installed/ +# - build/_deps/ +# key: "$CI_COMMIT_REF_SLUG" +# policy: pull-push + + +# # Linux Build (CachyOS optimized) +# build-linux: +# stage: build +# image: cachyos/cachyos:latest +# tags: +# - citron-build +# before_script: +# - pacman -Syu --noconfirm +# - pacman -S --noconfirm alsa-lib base-devel boost boost-libs catch2 cmake curl ffmpeg fmt fuse2 gamemode gcc gdb git glslang glu libusb libxi libxkbcommon libxkbcommon-x11 libxss libzip lz4 mbedtls2 mesa nasm ninja nlohmann-json openal openssl opus perl pipewire-audio pulseaudio pulseaudio-alsa qt6-base qt6-multimedia qt6-tools qt6-wayland sdl2 sdl2-compat unzip vulkan-headers vulkan-icd-loader vulkan-mesa-layers wget xcb-util-cursor xcb-util-image xcb-util-keysyms xcb-util-renderutil xcb-util-wm xorg-server-xvfb zip zstd zsync +# - echo '#!/bin/bash' > /usr/local/bin/shasum +# - echo 'if [ "$1" = "-a" ] && [ "$2" = "256" ]; then' >> /usr/local/bin/shasum +# - echo ' shift 2' >> /usr/local/bin/shasum +# - echo ' sha256sum "$@"' >> /usr/local/bin/shasum +# - echo 'else' >> /usr/local/bin/shasum +# - echo ' sha256sum "$@"' >> /usr/local/bin/shasum +# - echo 'fi' >> /usr/local/bin/shasum +# - chmod +x /usr/local/bin/shasum +# script: | +# # Clean and reset submodules to handle any inconsistencies +# git submodule deinit --all --force || true +# # Forcefully remove stale lock files that can block submodule updates +# find .git/modules -type f -name "*.lock" -delete +# git submodule update --init --recursive --force +# mkdir -p build && cd build +# # Dynamically find the Qt private header path +# HEADER_PATH=$(pacman -Ql qt6-base | grep 'qpa/qplatformnativeinterface.h$' | awk '{print $2}') +# if [ -z "$HEADER_PATH" ]; then echo "ERROR: Could not find qplatformnativeinterface.h path." >&2; exit 1; fi +# QT_PRIVATE_INCLUDE_DIR=$(dirname "$(dirname "$HEADER_PATH")") +# echo "Found Qt private include directory: ${QT_PRIVATE_INCLUDE_DIR}" +# # Configure and build with the dynamically found path +# cmake .. \ +# -G Ninja \ +# -DCMAKE_C_COMPILER=gcc \ +# -DCMAKE_CXX_COMPILER=g++ \ +# -DCMAKE_BUILD_TYPE=Release \ +# -DCMAKE_INSTALL_PREFIX=/usr \ +# -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ +# -DUSE_SYSTEM_QT=ON \ +# -DCMAKE_C_FLAGS="-O3 -ffast-math -funsafe-math-optimizations -fno-math-errno -fno-trapping-math -freciprocal-math -ffp-contract=fast -fassociative-math -fno-rounding-math -fno-signaling-nans -march=x86-64-v2 -mtune=native -fomit-frame-pointer -fstrict-aliasing -frename-registers -fno-plt -funroll-loops -floop-interchange -floop-strip-mine -floop-block -fprefetch-loop-arrays -fpredictive-commoning -ftree-vectorize -fsplit-paths -finline-functions -finline-limit=1000 -finline-functions-called-once -fgcse-sm -fgcse-las -freorder-blocks-and-partition -fipa-pta -fipa-cp-clone -fwhole-program -flto=auto -fuse-linker-plugin -fvisibility=hidden -fdata-sections -ffunction-sections -fno-semantic-interposition -fopenmp -fexceptions -frtti" \ +# -DCMAKE_CXX_FLAGS="-I${QT_PRIVATE_INCLUDE_DIR} -O3 -ffast-math -funsafe-math-optimizations -fno-math-errno -fno-trapping-math -freciprocal-math -ffp-contract=fast -fassociative-math -fno-rounding-math -fno-signaling-nans -march=x86-64-v2 -mtune=native -fomit-frame-pointer -fstrict-aliasing -frename-registers -fno-plt -funroll-loops -floop-interchange -floop-strip-mine -floop-block -fprefetch-loop-arrays -fpredictive-commoning -ftree-vectorize -fsplit-paths -finline-functions -finline-limit=1000 -finline-functions-called-once -fgcse-sm -fgcse-las -freorder-blocks-and-partition -fipa-pta -fipa-cp-clone -fwhole-program -flto=auto -fuse-linker-plugin -fvisibility=hidden -fdata-sections -ffunction-sections -fno-semantic-interposition -fopenmp -fexceptions -frtti -Wno-error -w" \ +# -DCMAKE_EXE_LINKER_FLAGS="-Wl,--gc-sections -Wl,-O2 -Wl,--as-needed" \ +# -DSDL_PIPEWIRE=OFF \ +# -DCITRON_USE_BUNDLED_VCPKG=OFF \ +# -DUSE_DISCORD_PRESENCE=OFF \ +# -DBUNDLE_SPEEX=ON \ +# -DCITRON_USE_BUNDLED_FFMPEG=OFF \ +# -DCITRON_USE_QT_MULTIMEDIA=OFF \ +# -DCITRON_USE_QT_WEB_ENGINE=OFF +# ninja +# cd .. +# echo "Build completed, checking for executables..." +# ls -la build/bin/ || echo "No build/bin directory found" +# find build/ -name "*.so" -o -name "citron*" | head -20 || echo "No shared libraries or executables found" +# echo "Checking library dependencies..." +# ldd build/bin/citron || echo "citron binary not found or ldd failed" +# chmod +x AppImage-build-local.sh +# echo "Attempting AppImage build with FUSE workaround..." +# export APPIMAGE_EXTRACT_AND_RUN=1 +# export ARCH=x86_64 +# export VERSION=$(git describe --tags --always) +# echo "Testing shasum compatibility..." +# which shasum || echo "shasum not found in PATH" +# echo "Testing shasum -a 256 syntax..." +# echo "test" | shasum -a 256 || echo "shasum -a 256 failed" +# echo "test" | sha256sum || echo "sha256sum failed" +# echo "Running AppImage build..." +# ./AppImage-build-local.sh || echo "AppImage build failed, continuing..." +# ls -la *.AppImage || echo "No AppImage files found" +# echo "AppImage size information:" +# du -h *.AppImage || echo "No AppImage to measure" +# echo "Total artifact size:" +# du -sh citron.AppImage build/bin/citron 2>/dev/null || echo "Cannot measure artifact sizes" +# echo "Compressing AppImage for upload..." +# gzip -9 -c citron.AppImage > citron.AppImage.gz +# ls -la citron.AppImage.gz +# du -h citron.AppImage.gz +# echo "AppImage compressed with gzip successfully" +# tar -czf citron-binary.tar.gz build/bin/ +# ls -la citron-binary.tar.gz +# du -h citron-binary.tar.gz +# artifacts: +# paths: +# - citron.AppImage.gz +# - citron-binary.tar.gz +# expire_in: 1 week +# when: always +# only: +# - main +# - master +# - develop +# - ci-fixes +# - Boss-Build-Flags-For-CI-CMAKE-ON-CachyOS +# - merge_requests + +# # Linux Build (CachyOS v3 optimized) +# build-linux-v3: +# stage: build +# image: cachyos/cachyos-v3 +# tags: +# - citron-build +# before_script: +# - pacman -Syu --noconfirm +# - pacman -S --noconfirm alsa-lib base-devel boost boost-libs catch2 cmake curl ffmpeg fmt fuse2 gamemode gcc gdb git glslang glu libusb libxi libxkbcommon libxkbcommon-x11 libxss libzip lz4 mbedtls2 mesa nasm ninja nlohmann-json openal openssl opus perl pipewire-audio pulseaudio pulseaudio-alsa python-pip qt6-base qt6-multimedia qt6-tools qt6-wayland sdl2 sdl2-compat unzip vulkan-headers vulkan-icd-loader vulkan-mesa-layers wget xcb-util-cursor xcb-util-image xcb-util-keysyms xcb-util-renderutil xcb-util-wm xorg-server-xvfb zip zstd zsync +# - echo '#!/bin/bash' > /usr/local/bin/shasum +# - echo 'if [ "$1" = "-a" ] && [ "$2" = "256" ]; then' >> /usr/local/bin/shasum +# - echo ' shift 2' >> /usr/local/bin/shasum +# - echo ' sha256sum "$@"' >> /usr/local/bin/shasum +# - echo 'else' >> /usr/local/bin/shasum +# - echo ' sha256sum "$@"' >> /usr/local/bin/shasum +# - echo 'fi' >> /usr/local/bin/shasum +# - chmod +x /usr/local/bin/shasum +# script: | +# git submodule deinit --all --force || true +# find .git/modules -type f -name "*.lock" -delete +# git submodule update --init --recursive --force +# mkdir -p build && cd build +# HEADER_PATH=$(pacman -Ql qt6-base | grep 'qpa/qplatformnativeinterface.h$' | awk '{print $2}') +# if [ -z "$HEADER_PATH" ]; then echo "ERROR: Could not find qplatformnativeinterface.h path." >&2; exit 1; fi +# QT_PRIVATE_INCLUDE_DIR=$(dirname "$(dirname "$HEADER_PATH")") +# echo "Found Qt private include directory: ${QT_PRIVATE_INCLUDE_DIR}" +# cmake .. \ +# -G Ninja \ +# -DCMAKE_C_COMPILER=gcc \ +# -DCMAKE_CXX_COMPILER=g++ \ +# -DCMAKE_BUILD_TYPE=Release \ +# -DCMAKE_INSTALL_PREFIX=/usr \ +# -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ +# -DCMAKE_C_FLAGS="-O3 -ffast-math -funsafe-math-optimizations -fno-math-errno -fno-trapping-math -freciprocal-math -ffp-contract=fast -fassociative-math -fno-rounding-math -fno-signaling-nans -march=x86-64-v3 -mtune=native -fomit-frame-pointer -fstrict-aliasing -frename-registers -fno-plt -funroll-loops -floop-interchange -floop-strip-mine -floop-block -fprefetch-loop-arrays -fpredictive-commoning -ftree-vectorize -fsplit-paths -finline-functions -finline-limit=1000 -finline-functions-called-once -fgcse-sm -fgcse-las -freorder-blocks-and-partition -fipa-pta -fipa-cp-clone -fwhole-program -flto=auto -fuse-linker-plugin -fvisibility=hidden -fdata-sections -ffunction-sections -fno-semantic-interposition -fopenmp -fexceptions -frtti" \ +# -DCMAKE_CXX_FLAGS="-I${QT_PRIVATE_INCLUDE_DIR} -O3 -ffast-math -funsafe-math-optimizations -fno-math-errno -fno-trapping-math -freciprocal-math -ffp-contract=fast -fassociative-math -fno-rounding-math -fno-signaling-nans -march=x86-64-v3 -mtune=native -fomit-frame-pointer -fstrict-aliasing -frename-registers -fno-plt -funroll-loops -floop-interchange -floop-strip-mine -floop-block -fprefetch-loop-arrays -fpredictive-commoning -ftree-vectorize -fsplit-paths -finline-functions -finline-limit=1000 -finline-functions-called-once -fgcse-sm -fgcse-las -freorder-blocks-and-partition -fipa-pta -fipa-cp-clone -fwhole-program -flto=auto -fuse-linker-plugin -fvisibility=hidden -fdata-sections -ffunction-sections -fno-semantic-interposition -fopenmp -fexceptions -frtti -Wno-error -w" \ +# -DCMAKE_EXE_LINKER_FLAGS="-Wl,--gc-sections -Wl,-O2 -Wl,--as-needed" \ +# -DSDL_PIPEWIRE=OFF \ +# -DCITRON_USE_BUNDLED_VCPKG=OFF \ +# -DUSE_DISCORD_PRESENCE=OFF \ +# -DBUNDLE_SPEEX=ON \ +# -DCITRON_USE_BUNDLED_FFMPEG=OFF \ +# -DCITRON_USE_QT_MULTIMEDIA=OFF \ +# -DCITRON_USE_QT_WEB_ENGINE=OFF +# ninja +# cd .. +# echo "Build completed, checking for executables..." +# ls -la build/bin/ || echo "No build/bin directory found" +# find build/ -name "*.so" -o -name "citron*" | head -20 || echo "No shared libraries or executables found" +# echo "Checking library dependencies..." +# ldd build/bin/citron || echo "citron binary not found or ldd failed" +# chmod +x AppImage-build-local.sh +# echo "Attempting AppImage build with FUSE workaround..." +# export APPIMAGE_EXTRACT_AND_RUN=1 +# export ARCH=x86_64 +# export VERSION=$(git describe --tags --always) +# echo "Testing shasum compatibility..." +# which shasum || echo "shasum not found in PATH" +# echo "Testing shasum -a 256 syntax..." +# echo "test" | shasum -a 256 || echo "shasum -a 256 failed" +# echo "test" | sha256sum || echo "sha256sum failed" +# echo "Running AppImage build..." +# ./AppImage-build-local.sh || echo "AppImage build failed, continuing..." +# ls -la *.AppImage || echo "No AppImage files found" +# echo "AppImage size information:" +# du -h *.AppImage || echo "No AppImage to measure" +# echo "Total artifact size:" +# du -sh citron.AppImage build/bin/citron 2>/dev/null || echo "Cannot measure artifact sizes" +# echo "Compressing AppImage for upload..." +# gzip -9 -c citron.AppImage > citron.AppImage.gz +# ls -la citron.AppImage.gz +# du -h citron.AppImage.gz +# echo "AppImage compressed with gzip successfully" +# tar -czf citron-binary.tar.gz build/bin/ +# ls -la citron-binary.tar.gz +# du -h citron-binary.tar.gz +# artifacts: +# paths: +# - citron.AppImage.gz +# - citron-binary.tar.gz +# expire_in: 1 week +# when: always +# only: +# - main +# - master +# - develop +# - ci-fixes +# - Boss-Build-Flags-For-CI-CMAKE-ON-CachyOS +# - merge_requests + +# # Linux Build (Steam Deck optimized - AMD Zen 2) +# build-linux-steamdeck: +# stage: build +# image: cachyos/cachyos:latest +# tags: +# - citron-build +# before_script: +# - pacman -Syu --noconfirm +# - pacman -S --noconfirm alsa-lib base-devel boost boost-libs catch2 cmake curl ffmpeg fmt fuse2 gamemode gcc gdb git glslang glu libusb libxi libxkbcommon libxkbcommon-x11 libxss libzip lz4 mbedtls2 mesa nasm ninja nlohmann-json openal openssl opus perl pipewire-audio pulseaudio pulseaudio-alsa python-pip qt6-base qt6-multimedia qt6-tools qt6-wayland sdl2 sdl2-compat unzip vulkan-headers vulkan-icd-loader vulkan-mesa-layers wget xcb-util-cursor xcb-util-image xcb-util-keysyms xcb-util-renderutil xcb-util-wm xorg-server-xvfb zip zstd zsync +# - echo '#!/bin/bash' > /usr/local/bin/shasum +# - echo 'if [ "$1" = "-a" ] && [ "$2" = "256" ]; then' >> /usr/local/bin/shasum +# - echo ' shift 2' >> /usr/local/bin/shasum +# - echo ' sha256sum "$@"' >> /usr/local/bin/shasum +# - echo 'else' >> /usr/local/bin/shasum +# - echo ' sha256sum "$@"' >> /usr/local/bin/shasum +# - echo 'fi' >> /usr/local/bin/shasum +# - chmod +x /usr/local/bin/shasum +# script: | +# git submodule deinit --all --force || true +# find .git/modules -type f -name "*.lock" -delete +# git submodule update --init --recursive --force +# mkdir -p build && cd build +# HEADER_PATH=$(pacman -Ql qt6-base | grep 'qpa/qplatformnativeinterface.h$' | awk '{print $2}') +# if [ -z "$HEADER_PATH" ]; then echo "ERROR: Could not find qplatformnativeinterface.h path." >&2; exit 1; fi +# QT_PRIVATE_INCLUDE_DIR=$(dirname "$(dirname "$HEADER_PATH")") +# echo "Found Qt private include directory: ${QT_PRIVATE_INCLUDE_DIR}" +# cmake .. \ +# -G Ninja \ +# -DCMAKE_C_COMPILER=gcc \ +# -DCMAKE_CXX_COMPILER=g++ \ +# -DCMAKE_BUILD_TYPE=Release \ +# -DCMAKE_INSTALL_PREFIX=/usr \ +# -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ +# -DCMAKE_C_FLAGS="-O3 -ffast-math -funsafe-math-optimizations -fno-math-errno -fno-trapping-math -freciprocal-math -ffp-contract=fast -fassociative-math -fno-rounding-math -fno-signaling-nans -march=znver2 -mtune=znver2 -fomit-frame-pointer -fstrict-aliasing -frename-registers -fno-plt -funroll-loops -floop-interchange -floop-strip-mine -floop-block -fprefetch-loop-arrays -fpredictive-commoning -ftree-vectorize -fsplit-paths -finline-functions -finline-limit=1000 -finline-functions-called-once -fgcse-sm -fgcse-las -freorder-blocks-and-partition -fipa-pta -fipa-cp-clone -fwhole-program -flto=auto -fuse-linker-plugin -fvisibility=hidden -fdata-sections -ffunction-sections -fno-semantic-interposition -fopenmp -fexceptions -frtti" \ +# -DCMAKE_CXX_FLAGS="-I${QT_PRIVATE_INCLUDE_DIR} -O3 -ffast-math -funsafe-math-optimizations -fno-math-errno -fno-trapping-math -freciprocal-math -ffp-contract=fast -fassociative-math -fno-rounding-math -fno-signaling-nans -march=znver2 -mtune=znver2 -fomit-frame-pointer -fstrict-aliasing -frename-registers -fno-plt -funroll-loops -floop-interchange -floop-strip-mine -floop-block -fprefetch-loop-arrays -fpredictive-commoning -ftree-vectorize -fsplit-paths -finline-functions -finline-limit=1000 -finline-functions-called-once -fgcse-sm -fgcse-las -freorder-blocks-and-partition -fipa-pta -fipa-cp-clone -fwhole-program -flto=auto -fuse-linker-plugin -fvisibility=hidden -fdata-sections -ffunction-sections -fno-semantic-interposition -fopenmp -fexceptions -frtti -Wno-error -w" \ +# -DCMAKE_EXE_LINKER_FLAGS="-Wl,--gc-sections -Wl,-O2 -Wl,--as-needed" \ +# -DSDL_PIPEWIRE=OFF \ +# -DCITRON_USE_BUNDLED_VCPKG=OFF \ +# -DUSE_DISCORD_PRESENCE=OFF \ +# -DBUNDLE_SPEEX=ON \ +# -DCITRON_USE_BUNDLED_FFMPEG=OFF \ +# -DCITRON_USE_QT_MULTIMEDIA=OFF \ +# -DCITRON_USE_QT_WEB_ENGINE=OFF +# ninja +# cd .. +# echo "Build completed, checking for executables..." +# ls -la build/bin/ || echo "No build/bin directory found" +# find build/ -name "*.so" -o -name "citron*" | head -20 || echo "No shared libraries or executables found" +# echo "Checking library dependencies..." +# ldd build/bin/citron || echo "citron binary not found or ldd failed" +# chmod +x AppImage-build-local.sh +# echo "Attempting AppImage build with FUSE workaround..." +# export APPIMAGE_EXTRACT_AND_RUN=1 +# export ARCH=x86_64 +# export VERSION=$(git describe --tags --always) +# echo "Testing shasum compatibility..." +# which shasum || echo "shasum not found in PATH" +# echo "Testing shasum -a 256 syntax..." +# echo "test" | shasum -a 256 || echo "shasum -a 256 failed" +# echo "test" | sha256sum || echo "sha256sum failed" +# echo "Running AppImage build..." +# ./AppImage-build-local.sh || echo "AppImage build failed, continuing..." +# ls -la *.AppImage || echo "No AppImage files found" +# echo "AppImage size information:" +# du -h *.AppImage || echo "No AppImage to measure" +# echo "Total artifact size:" +# du -sh citron.AppImage build/bin/citron 2>/dev/null || echo "Cannot measure artifact sizes" +# echo "Compressing AppImage for upload..." +# gzip -9 -c citron.AppImage > citron.AppImage.gz +# ls -la citron.AppImage.gz +# du -h citron.AppImage.gz +# echo "AppImage compressed with gzip successfully" +# tar -czf citron-binary.tar.gz build/bin/ +# ls -la citron-binary.tar.gz +# du -h citron-binary.tar.gz +# artifacts: +# paths: +# - citron.AppImage.gz +# - citron-binary.tar.gz +# expire_in: 1 week +# when: always +# only: +# - main +# - master +# - develop +# - ci-fixes +# - Boss-Build-Flags-For-CI-CMAKE-ON-CachyOS +# - merge_requests + +# # Android Build (Ubuntu 24.04 optimized) +# build-android: +# stage: build +# image: ubuntu:24.04 +# tags: +# - citron-build +# before_script: +# - apt-get update -qq +# - apt-get install -y -qq openjdk-17-jdk wget unzip curl git cmake build-essential pkg-config zip glslang-tools +# - wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip +# - unzip -q commandlinetools-linux-11076708_latest.zip +# - mkdir -p android-sdk/cmdline-tools +# - mv cmdline-tools android-sdk/cmdline-tools/latest +# - export ANDROID_SDK_ROOT=$PWD/android-sdk +# - export PATH=$PATH:$ANDROID_SDK_ROOT/cmdline-tools/latest/bin +# - printf "y\ny\ny\ny\ny\ny\ny\n" | sdkmanager --licenses || true +# - sdkmanager "platform-tools" "platforms;android-34" "build-tools;34.0.0" +# - wget -q https://dl.google.com/android/repository/android-ndk-r26b-linux.zip +# - unzip -q android-ndk-r26b-linux.zip +# - export ANDROID_NDK_HOME=$PWD/android-ndk-r26b +# script: +# - git submodule deinit --all --force || true +# - git submodule update --init --recursive --force +# - cmake -B build-android -S . -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-30 -DENABLE_QT=OFF -DENABLE_SDL2=OFF -DENABLE_WEB_SERVICE=OFF -DCITRON_USE_BUNDLED_VCPKG=ON -DCITRON_USE_BUNDLED_FFMPEG=ON -DANDROID_ARM_NEON=ON -DCITRON_ENABLE_LTO=ON +# - cmake --build build-android --config $BUILD_TYPE --parallel $CMAKE_BUILD_PARALLEL_LEVEL +# - cd src/android +# - ./gradlew assembleMainlineRelease +# artifacts: +# paths: +# - src/android/app/build/outputs/apk/mainline/release/*.apk +# expire_in: 1 week +# only: +# - main +# - master +# - develop +# - ci-fixes +# - merge_requests +# allow_failure: true + +# # Unit Tests +# test-unit: +# stage: test +# image: ubuntu:24.04 +# tags: +# - citron-build +# before_script: +# # Set the environment to be non-interactive +# - export DEBIAN_FRONTEND=noninteractive +# +# # Update package lists and install tool for managing repositories +# - apt-get update -qq +# - apt-get install -y -qq software-properties-common +# +# # Reliably add the 'universe' and 'multiverse' repositories +# - add-apt-repository universe +# - add-apt-repository multiverse +# +# # Update the package lists again to include the new repositories +# - apt-get update -qq +# +# # Install all required system packages for the build +# - apt-get install -y -qq +# build-essential +# cmake +# ninja-build +# git +# pkg-config +# catch2 +# libasound2-dev +# libpulse-dev +# libboost-all-dev +# libavcodec-dev +# libavfilter-dev +# libavformat-dev +# libavutil-dev +# libswscale-dev +# llvm-17-dev +# gamemode-dev +# libenet-dev +# libfmt-dev +# liblz4-dev +# libopus-dev +# libssl-dev +# libusb-1.0-0-dev +# libvulkan-dev +# nlohmann-json3-dev +# zlib1g-dev +# libzstd-dev +# libopenal-dev +# libedit-dev +# libcurl4-openssl-dev +# libstb-dev +# libcubeb-dev +# libspeexdsp-dev +# libva-dev +# glslang-tools +# libx11-dev +# + +# script: +# - git submodule deinit --all --force || true +# - git submodule update --init --recursive --force +# - cmake -B build-test -S . -DCMAKE_BUILD_TYPE=Debug -DCITRON_TESTS=ON -DENABLE_QT=OFF -DENABLE_SDL2=OFF -DENABLE_WEB_SERVICE=OFF +# - cmake --build build-test --config Debug --parallel $CMAKE_BUILD_PARALLEL_LEVEL +# - cd build-test +# - ctest --output-on-failure +# artifacts: +# reports: +# junit: build-test/Testing/**/*.xml +# expire_in: 1 week +# only: +# - main +# - master +# - develop +# - ci-fixes +# - merge_requests + +# # Package Release +# package-release: +# stage: package +# image: ubuntu:24.04 +# tags: +# - citron-build +# dependencies: +# - build-linux +# script: +# - mkdir -p release +# - echo "Creating release package..." +# - tar -czf citron-release-$(date +%Y%m%d).tar.gz release/ || true +# artifacts: +# paths: +# - citron-release-*.tar.gz +# expire_in: 1 month +# only: +# - main +# - master +# - develop +# - ci-fixes +# - tags \ No newline at end of file diff --git a/AppImage-build-debian-inner.sh b/AppImage-build-debian-inner.sh new file mode 100755 index 0000000000..34aa7252a7 --- /dev/null +++ b/AppImage-build-debian-inner.sh @@ -0,0 +1,83 @@ +#! /bin/bash +set -e + +# Make sure script is called from inside our container +test -e /tmp/torzu-src-ro || (echo "Script MUST NOT be called directly!" ; exit 1) + +# Set up environment +export LANG=C.UTF-8 +export LC_ALL=C.UTF-8 +unset LC_ADDRESS LC_NAME LC_MONETARY LC_PAPER LC_TELEPHONE LC_MEASUREMENT LC_TIME + +# Raise max open files count +ulimit -n 50000 + +# Install dependencies +apt -y install cmake ninja-build build-essential autoconf pkg-config locales wget git file mold libtool lsb-release wget software-properties-common gnupg \ + qtbase5-dev qtmultimedia5-dev qtbase5-private-dev glslang-tools libssl-dev libavcodec-dev libavfilter-dev libavutil-dev libswscale-dev libpulse-dev libasound2-dev +if [ ! "$BUILD_USE_CPM" = 1 ]; then + apt -y install libfmt-dev libenet-dev liblz4-dev nlohmann-json3-dev zlib1g-dev libopus-dev libsimpleini-dev libstb-dev libzstd-dev libusb-1.0-0-dev libcubeb-dev libcpp-jwt-dev libvulkan-dev gamemode-dev libasound2-dev libglu1-mesa-dev libxext-dev mesa-common-dev libva-dev + + if [ ! -f /usr/local/lib/cmake/Boost-1.88.0/BoostConfigVersion.cmake ]; then + # Install Boost + wget https://archives.boost.io/release/1.88.0/source/boost_1_88_0.tar.bz2 + echo "Extracting Boost sources..." + tar xf boost_1_88_0.tar.bz2 + cd boost_1_88_0 + ./bootstrap.sh + ./b2 install --with-{headers,context,system,fiber,atomic,filesystem} link=static + cd .. + rm -rf boost_1_88_0 boost_1_88_0.tar.bz2 + fi +fi + +# Install Clang +if ([ "$BUILD_USE_CLANG" = 1 ] && ! clang-19 --version); then + cd /tmp + wget https://apt.llvm.org/llvm.sh + chmod +x llvm.sh + ./llvm.sh 19 + rm llvm.sh +fi + +# Mount Torzu sources with temporary overlay +cd /tmp +mkdir torzu-src-upper torzu-src-work torzu-src +mount -t overlay overlay -olowerdir=torzu-src-ro,upperdir=torzu-src-upper,workdir=torzu-src-work torzu-src + +# Get extra configuration/compilation options +EXTRA_COMPILE_FLAGS="" +EXTRA_CMAKE_FLAGS="" +if [ "$BUILD_USE_CLANG" = 1 ]; then + EXTRA_CMAKE_FLAGS="-DCMAKE_C_COMPILER=clang-19 -DCMAKE_CXX_COMPILER=clang++-19" + FATLTO_FLAG="-flto=full" +else + FATLTO_FLAG="-flto" +fi +if [ "$BUILD_USE_THIN_LTO" = 1 ]; then + EXTRA_COMPILE_FLAGS="-flto=thin" +fi +if [ "$BUILD_USE_FAT_LTO" = 1 ]; then + EXTRA_COMPILE_FLAGS="$FATLTO_FLAG" +fi +if [ "$BUILD_USE_CPM" = 1 ]; then + EXTRA_CMAKE_FLAGS="$EXTRA_CMAKE_FLAGS -DYUZU_USE_CPM=ON" +fi + +# Build Torzu +cd /tmp +mkdir torzu-build +cd torzu-build +cmake /tmp/torzu-src -GNinja -DCMAKE_BUILD_TYPE=Release -DYUZU_TESTS=OFF -DENABLE_QT_TRANSLATION=OFF -DSPIRV_WERROR=OFF -DCMAKE_FIND_LIBRARY_SUFFIXES=".a;.so" -DSPIRV-Headers_SOURCE_DIR=/tmp/torzu-src/externals/SPIRV-Headers -DCMAKE_{C,CXX}_FLAGS="$EXTRA_COMPILE_FLAGS -fdata-sections -ffunction-sections" -DCMAKE_{EXE,SHARED}_LINKER_FLAGS="-Wl,--gc-sections" $EXTRA_CMAKE_FLAGS +ninja || ( + echo "Compilation has failed. Dropping you into a shell so you can inspect the situation. Run 'ninja' to retry and exit shell once compilation has finished successfully." + echo "Note that any changes made here will not be reflected to the host environment, but changes made from the host environment will be reflected here." + bash +) + +# Generate AppImage +cp -rv /tmp/torzu-src/AppImageBuilder /tmp/AppImageBuilder +cd /tmp/AppImageBuilder +./build.sh /tmp/torzu-build /tmp/torzu.AppImage || echo "This error is known. Using workaround..." +cp /lib/$(uname -m)-linux-gnu/libICE.so.6 build/ +mv build /tmp/hosttmp/torzu-debian-appimage-rootfs diff --git a/AppImage-build-debian.sh b/AppImage-build-debian.sh new file mode 100755 index 0000000000..3a8836e042 --- /dev/null +++ b/AppImage-build-debian.sh @@ -0,0 +1,73 @@ +#! /bin/bash +set -e + +# Parse options +for i in "$@" +do +case $i in + -l|--clang) + export BUILD_USE_CLANG=1 + echo "-> Using Clang for compilation." + ;; + -o|--thin-lto) + export BUILD_USE_THIN_LTO=1 + echo "-> Thin link time optimization enabled." + ;; + -O|--fat-lto) + export BUILD_USE_FAT_LTO=1 + echo "-> Fat link time optimization enabled." + ;; + -p|--use-cpm) + export BUILD_USE_CPM=1 + echo "-> Using CPM to download most dependencies." + ;; + -k|--keep-rootfs) + BUILD_KEEP_ROOTFS=1 + echo "-> Not deleting rootfs after successful build." + ;; + *) + echo "Usage: $0 [--clang/-l] [--thin-lto/-o] [--fat-lto/-O] [--use-cpm/-p] [--keep-rootfs/-k]" + exit 1 + ;; +esac +done + +# Make sure options are valid +if [ "$BUILD_USE_THIN_LTO" = 1 ] && [ "$BUILD_USE_CLANG" != 1 ]; then + echo "Thin LTO can't be used without Clang!" + exit 2 +fi +if [ "$BUILD_USE_THIN_LTO" = 1 ] && [ "$BUILD_USE_FAT_LTO" = 1 ]; then + echo "Only either thin or fat LTO can be used!" + exit 2 +fi + +# Get citron source dir +citron_SOURCE_DIR="$(realpath "$(dirname "${BASH_SOURCE[0]}")")" +echo "-> Source dir is $citron_SOURCE_DIR" +rm -rf "$citron_SOURCE_DIR/AppImageBuilder/build" + +# Generate debian rootfs +cd /tmp +echo "Cleaning up before build..." +rm -rf citron-debian-appimage-rootfs +[ -d rootfs-citron-appimage-build ] || + debootstrap stable rootfs-citron-appimage-build http://deb.debian.org/debian/ +bwrap --bind rootfs-citron-appimage-build / \ + --unshare-pid \ + --dev-bind /dev /dev --proc /proc --tmpfs /tmp --ro-bind /sys /sys --dev-bind /run /run \ + --tmpfs /var/tmp \ + --chmod 1777 /tmp \ + --ro-bind /etc/resolv.conf /etc/resolv.conf \ + --ro-bind "$citron_SOURCE_DIR" /tmp/citron-src-ro \ + --chdir / \ + --tmpfs /home \ + --setenv HOME /home \ + --bind /tmp /tmp/hosttmp \ + /tmp/citron-src-ro/AppImage-build-debian-inner.sh +appimagetool citron-debian-appimage-rootfs citron.AppImage +echo "AppImage generated at /tmp/citron.AppImage! Cleaning up..." +rm -rf citron-debian-appimage-rootfs +if [ ! "$BUILD_KEEP_ROOTFS" = 1 ]; then + rm -rf rootfs-citron-appimage-build +fi diff --git a/AppImage-build-local.sh b/AppImage-build-local.sh new file mode 100755 index 0000000000..81eda3febf --- /dev/null +++ b/AppImage-build-local.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +FILE=build/bin/citron +if test -f "$FILE"; then + # remove any previously made AppImage in the base citron git folder + rm ./citron.AppImage + + # enter AppImage utility folder + cd AppImageBuilder + + # run the build script to create the AppImage + # (usage) ./build.sh [source citron build folder] [destination .AppImage file] + ./build.sh ../build ./citron.AppImage + + FILE=./citron.AppImage + if test -f "$FILE"; then + # move the AppImage to the main citron folder + mv citron.AppImage .. + # return to main citron folder + cd .. + # show contents of current folder + echo + ls + # show AppImages specifically + echo + ls *.AppImage + echo + echo "'citron.AppImage' is now located in the current folder." + echo + else + cd .. + echo "AppImage was not built." + fi +else + echo + echo "$FILE does not exist." + echo + echo "No citron executable found in the /citron/build/bin folder!" + echo + echo "You must first build a native linux version of citron before running this script!" + echo +fi diff --git a/AppImageBuilder/build-v2.sh b/AppImageBuilder/build-v2.sh new file mode 100755 index 0000000000..3d4273fe33 --- /dev/null +++ b/AppImageBuilder/build-v2.sh @@ -0,0 +1,104 @@ +#!/bin/bash +set -e + +# Get script directory +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +BUILD_DIR_RAW="${1:-$SCRIPT_DIR/../build/generate}" +BUILD_DIR="$(readlink -f "${BUILD_DIR_RAW}")" + +# 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 +export NO_STRIP=1 # Avoid strip failures on modern binaries (SHT_RELR) + +# 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" diff --git a/AppImageBuilder/build.sh b/AppImageBuilder/build.sh new file mode 100755 index 0000000000..13f1c45ce1 --- /dev/null +++ b/AppImageBuilder/build.sh @@ -0,0 +1,170 @@ +#! /bin/bash +set -e + +# Check arguments +if [[ $# != 2 ]]; then + >&2 echo "Bad usage!" + echo "Usage: $0 " + exit 1 +fi + +# Get paths +ARCH="$(uname -m)" +SYSTEM_LIBS="/usr/lib" +SYSTEM_LIBS64="/usr/lib64" +citron_BIN="${1}/bin" +citron_BIN_GUI="${citron_BIN}/citron" + +# Make sure executable exists +if [[ $(file -b --mime-type "$citron_BIN_GUI") != application/x-pie-executable ]]; then + >&2 echo "Invalid citron executable!" +fi + +# Clean up build dir +rm -rf build +mkdir build + +# NOTE: some of these aren't used now, but can be reordered in priority when citron is converted to QT6 +# QT5 - /usr/lib/${ARCH}-linux-gnu/qt5 (debian), /usr/lib64/qt5 (fedora), /usr/lib/qt (steam deck) +# QT5 - /usr/lib/${ARCH}-linux-gnu/qt6 (debian), /usr/lib64/qt6 (fedora), /usr/lib/qt6 (steam deck) +QTFOUND="true" +QTDIR="$SYSTEM_LIBS"/${ARCH}-linux-gnu/qt5/plugins +if [ ! -d "$QTDIR" ]; then + # default qt5 folder not found, check for 64-bit qt5 folder + QTDIR="$SYSTEM_LIBS64"/qt5/plugins + if [ ! -d "$QTDIR" ]; then + # 64-bit qt5 folder not found, check for Steam Deck qt (qt5) folder + QTDIR="$SYSTEM_LIBS"/qt/plugins + if [ ! -d "$QTDIR" ]; then + # Steam Deck qt (qt5) folder not found, check for regular qt6 folder + QTDIR="$SYSTEM_LIBS"/${ARCH}-linux-gnu/qt6/plugins + if [ ! -d "$QTDIR" ]; then + # regular qt6 folder not found, check for 64-bit qt6 folder + QTDIR="$SYSTEM_LIBS64"/qt6/plugins + if [ ! -d "$QTDIR" ]; then + # 64-bit qt6 folder not found, check for Steam Deck qt6 folder + QTDIR="$SYSTEM_LIBS"/qt6/plugins + if [ ! -d "$QTDIR" ]; then + QTFOUND="false" + fi + fi + fi + fi + fi +fi +if [ $QTFOUND == "true" ]; then + echo "QT plugins from $QTDIR will be used." + + # Copy system dependencies used to build and required by the citron binary + # includes: + # - '/lib64/ld-linux-x86-64.so.2' or `/lib/ld-linux-aarch64.so.1` file per architecture + # - required files from `/usr/lib/x86_64-linux-gnu` or `/usr/lib/aarch64-linux-gnu` + # - different for SteamDeck, but still does it automatically + function copy_libs { + for lib in $(ldd "$1"); do + (cp -vn "$lib" ./build/ 2> /dev/null) || true + done + } + echo "Copying main dependencies..." + copy_libs "$citron_BIN_GUI" + + # Copy QT dependency folders, path determined above + echo "Copying Qt dependencies..." + mkdir ./build/qt5 + cp -rv "$QTDIR"/{imageformats,platforms,platformthemes,xcbglintegrations} ./build/qt5/ + + # Discover indirect dependencies (mostly from runtime-loaded Qt plugins) + echo "Copying extra dependencies..." + while true; do + LIBS="$(find ./build -name \*.so\*)" + LIB_COUNT=$(echo "$LIBS" | wc -l) + echo "$LIB_COUNT dependency libraries discovered so far..." + if [ $LIB_COUNT == "$PREV_LIB_COUNT" ]; then + break + fi + PREV_LIB_COUNT=$LIB_COUNT + + for plib in $LIBS; do + if [ -f "$plib" ]; then + copy_libs "$plib" + fi + done + done + + # Copy executable + cp -v "$citron_BIN_GUI" ./build/ + + # Copy assets for the appropriate arch + cp -v ./assets_"${ARCH}"/* ./build/ + # Copy common assets + cp -v ./assets/* ./build/ + + # Strip all libraries and executables + for file in $(find ./build -type f); do + (strip -v "$file" 2> /dev/null) || true + done + + PASSED_CHECKSUM="false" + FILE=appimagetool.AppImage + # total number of times to try downloading if a checksum doesn't match + DL_TRIES=3 + while [ $PASSED_CHECKSUM == "false" ] && [ "$DL_TRIES" -gt 0 ]; do + case $ARCH in + x86_64) + # Static copy from the 'ext-linux-bin' repo. + # Checksum will need to be changed when/if this file in the repo is updated. + if ! test -f "$FILE"; then + echo "Downloading appimagetool for architecture '$ARCH'" + wget -O appimagetool.AppImage https://github.com/litucks/ext-linux-bin/raw/refs/heads/main/appimage/appimagetool-x86_64.AppImage + fi + if [ $(shasum -a 256 appimagetool.AppImage | cut -d' ' -f1) = "110751478abece165a18460acbd7fd1398701f74a9405ad8ac053427d937bd5d" ] ; then + PASSED_CHECKSUM="true" + fi + # DISABLED TO USE THE ABOVE + # The current continuous release channel option, until a static copy is put in 'ext-linux-bin'. + # The checksum will pass until the continuous release is updated, then a new one needs to be + # generated to update this script. + #if ! test -f "$FILE"; then + # echo "Downloading appimagetool for architecture '$ARCH'" + # wget -O appimagetool.AppImage https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage + #fi + #if [ $(shasum -a 256 appimagetool.AppImage | cut -d' ' -f1) = "46fdd785094c7f6e545b61afcfb0f3d98d8eab243f644b4b17698c01d06083d1" ] ; then + # PASSED_CHECKSUM="true" + #fi + ;; + aarch64) + # Currently set to the continuous release channel until a static copy is put in 'ext-linux-bin'. + # The checksum will pass until the continuous release is updated, then a new one needs to be + # generated to update this script. + if ! test -f "$FILE"; then + echo "Downloading appimagetool for architecture '$ARCH'" + wget -O appimagetool.AppImage https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-aarch64.AppImage + fi + if [ $(shasum -a 256 appimagetool.AppImage | cut -d' ' -f1) = "04f45ea45b5aa07bb2b071aed9dbf7a5185d3953b11b47358c1311f11ea94a96" ] ; then + PASSED_CHECKSUM="true" + fi + ;; + *) + PASSED_CHECKSUM="invalid_arch" + ;; + esac + # delete the appimagetool downloaded if the checksum doesn't match. + if [ ! $PASSED_CHECKSUM == "true" ]; then + rm -f appimagetool.AppImage + fi + ((DL_TRIES-=1)) + done + if [ $PASSED_CHECKSUM == "true" ]; then + echo "Checksum passed. Proceeding to build image." + # Build AppImage + chmod a+x appimagetool.AppImage + ./appimagetool.AppImage ./build "$2" + elif [ $PASSED_CHECKSUM == "invalid_arch" ]; then + echo "No download found for architecture '$ARCH'. Building halted." + else + echo "Checksum for appimagetool does not match. Building halted." + echo "If the file to be downloaded has been changed, a new checksum will need to be generated for this script." + fi +else + echo "QT not found, aborting AppImage build." +fi diff --git a/AppImageBuilder/package-citron-linux.sh b/AppImageBuilder/package-citron-linux.sh deleted file mode 100644 index d1bbfc5d45..0000000000 --- a/AppImageBuilder/package-citron-linux.sh +++ /dev/null @@ -1,410 +0,0 @@ -#!/bin/sh -# SPDX-FileCopyrightText: 2026 citron Emulator Project -# SPDX-License-Identifier: GPL-3.0-or-later -# -# package-citron-linux.sh — Build AppImage + tar.zst using pkgforge tooling. -# -# Called by build-citron-linux.sh's build_appimage_stage() after the install -# tree has been staged under build_dir/install-root. -# Can also be invoked directly if the install tree is already in place. -# -# Environment variables: -# APP_VERSION Version string embedded in artifact filenames (required) -# ARCH CPU architecture (default: uname -m) -# ARCH_SUFFIX Extra suffix appended to filenames (e.g. _v3) -# DEVEL Set to 'true' to rename app to "citron nightly" in AppImage -# OUTPATH Output directory for finished artifacts (default: $PWD/dist) -# DESTDIR Staging root prepended to /usr paths when locating the -# citron binary, desktop file, icon, and Qt translations -# (default: "" — real /usr, e.g. for an already-installed -# system citron). Set by build-citron-linux.sh's -# build_appimage_stage() to a build-tree staging directory so -# no system files are read from /usr and nothing is written -# there. Does NOT apply to libpulse/libgamemode — those are -# genuine host runtime libraries, not part of citron's install. -# STRACE_MODE Enable dlopen/LD_DEBUG scan when citron starts up (default: 0) -# Set to 1 only if xvfb-run is installed; see comment below. -# DEPLOY_VULKAN Deploy Mesa/Vulkan DRI driver collection (default: 0) -# DEPLOY_OPENGL Deploy OpenGL/EGL/GLX libs into AppImage (default: 0) -# DEPLOY_PIPEWIRE Deploy PipeWire SPA/ALSA plugin trees (default: 0) -# DEPLOY_GTK Deploy GTK modules and gvfs into AppImage (default: 0) -# CITRON_QT_PATH Qt6 installation prefix (= QT_TARGET_PATH from CMake, -# e.g. .../cpmcache/qt-bin/6.9.3/gcc_64). Set automatically -# by build-citron-linux.sh. Used to derive QT_LOCATION so -# quick-sharun bundles CPM Qt plugins, not system Qt plugins. - -set -ex - -ARCH="${ARCH:-$(uname -m)}" - -if [ -z "${APP_VERSION:-}" ]; then - echo "Error: APP_VERSION environment variable is not set." >&2 - exit 1 -fi - -DESTDIR="${DESTDIR:-}" -OUTNAME_BASE="citron_nightly-${APP_VERSION}-linux-${ARCH}${ARCH_SUFFIX:-}" -OUTPATH="${OUTPATH:-$PWD/dist}" -export OUTNAME="${OUTNAME_BASE}.AppImage" -export DESKTOP="${DESTDIR}/usr/share/applications/org.citron_emu.citron.desktop" - -# Prefer a rasterised 256×256 PNG for .DirIcon — SVG fails silently as an -# AppImage icon on most desktop environments (appimaged, KDE, GNOME all expect -# a PNG for taskbar/launcher display). -# -# Priority: -# 1. Pre-built PNG installed by cmake (dist/org.citron_emu.citron.png → -# share/icons/hicolor/256x256/apps/); commit this to the emulator repo -# to make it permanent without needing any conversion tool. -# 2. Convert the installed SVG if a conversion tool is present. -# 3. Fall back to the raw SVG (icon will be broken on most desktops). -_png_from_repo="${DESTDIR}/usr/share/icons/hicolor/256x256/apps/org.citron_emu.citron.png" -_svg_icon="${DESTDIR}/usr/share/icons/hicolor/scalable/apps/org.citron_emu.citron.svg" -_png_icon="/tmp/citron-diricon-$$.png" -_try_svg2png() { - if command -v rsvg-convert >/dev/null 2>&1; then - rsvg-convert -w 256 -h 256 "$1" -o "$2" 2>/dev/null && return 0 - fi - if command -v inkscape >/dev/null 2>&1; then - inkscape --export-filename="$2" --export-width=256 --export-height=256 \ - "$1" >/dev/null 2>&1 && return 0 - fi - if command -v convert >/dev/null 2>&1; then - convert -background none -size 256x256 "$1" "$2" 2>/dev/null && return 0 - fi - if command -v magick >/dev/null 2>&1; then - magick -background none -size 256x256 "$1" "$2" 2>/dev/null && return 0 - fi - return 1 -} -if [ -f "$_png_from_repo" ]; then - export ICON="$_png_from_repo" -elif [ -f "$_svg_icon" ] && _try_svg2png "$_svg_icon" "$_png_icon"; then - export ICON="$_png_icon" -else - export ICON="$_svg_icon" -fi - -# ── quick-sharun deployment flags ─────────────────────────────────────────── -# -# STRACE_MODE=0 — disable the dlopen / LD_DEBUG scan entirely. -# -# In default mode (STRACE_MODE=1) quick-sharun spawns citron under -# LD_DEBUG=libs for STRACE_TIME (default 5 s), then kills it, to capture -# libs only dlopen'd at runtime. When xvfb-run is NOT installed, citron -# opens a real display window and the user must close it manually for the -# build to continue. Even with xvfb-run the scan is harmful for citron: -# everything it captures is stuff we explicitly do NOT want bundled — -# Vulkan ICD drivers (libvulkan_radeon.so, libvulkan_intel.so, -# libLLVM.so.20.1 at 137 MB, …) loaded by vkCreateInstance(), the full -# PipeWire SPA plugin tree loaded by PulseAudio's backend discovery, and -# GTK accessibility modules from the GNOME a11y bus. All of citron's -# legitimate runtime deps are already covered by the static ldd scan -# (explicit Qt plugins + libpulse + libgamemode). -export STRACE_MODE="${STRACE_MODE:-0}" -# -# DEPLOY_VULKAN=0 / DEPLOY_OPENGL=0 — do not stage Mesa DRI or the OpenGL set. -# NOTE: DEPLOY_VULKAN=0 does NOT suppress Vulkan ICDs captured by the dlopen -# scan (that requires STRACE_MODE=0 above); it only suppresses the separate -# static DEPLOY_VULKAN staging block (libVkLayer_*.so, etc.). -# libvulkan.so.1, libgbm.so, libdrm.so, libxcb-dri3.so, libxcb-glx.so are -# all DT_NEEDED of citron / Qt plugins and are captured by ldd regardless. -# SHARUN_ALLOW_SYS_VK_ICD=1 (set in .env below) makes sharun prefer the -# host GPU ICD at runtime, so we never need to bundle GPU-vendor drivers. -export DEPLOY_VULKAN="${DEPLOY_VULKAN:-0}" -export DEPLOY_OPENGL="${DEPLOY_OPENGL:-0}" -# -# DEPLOY_PIPEWIRE=0 — suppress the PipeWire/SPA/ALSA plugin staging block. -# On Ubuntu 24.04 PulseAudio is a PipeWire shim: ldd libpulse.so.0 shows -# libpipewire-0.3.so.0. quick-sharun's NEEDED_LIBS scan therefore sets -# DEPLOY_PIPEWIRE=1, staging pipewire-0.3/* + spa-0.2/**/* + alsa-lib/ -# *pipewire* (~41 PipeWire plugins + 11 ALSA plugins + Bluetooth codecs). -# These are backend-resolution plugins resolved from the HOST at runtime; -# bundling them bloats the AppImage and causes host-version conflicts. -# We carry libpulse.so.0 itself (passed explicitly below) — that is enough. -export DEPLOY_PIPEWIRE="${DEPLOY_PIPEWIRE:-0}" -# -# DEPLOY_GTK=0 — suppress GTK module / gvfs staging. -# quick-sharun includes libqgtk3.so in the Qt plugin glob and runs ldd on -# it; ldd sees libgtk-3.so.0, triggering DEPLOY_GTK=1 which stages -# gtk-3.0/immodules/*, gvfs/libgvfscommon.so, and gio/modules/* (the full -# GTK accessibility + filesystem integration stack). quick-sharun already -# sets QUICK_SHARUN_SKIP_DEPS_FOR="libqgtk3.so" so lib4bin skips transitive -# deps of that plugin — but the DETECTION loop fires before SKIP applies and -# sets DEPLOY_GTK=1 anyway. The GTK platform theme should dlopen the HOST -# GTK at runtime; bundling it causes version conflicts. -export DEPLOY_GTK="${DEPLOY_GTK:-0}" - -# Tell quick-sharun to use CPM's Qt installation for plugins, not the SYSTEM -# Qt. Without this, quick-sharun defaults to $LIB_DIR/qt6/plugins (system Qt), -# whose shared libraries depend on the SYSTEM libQt6Core.so.6. That system -# version may be older than the CPM-built Qt 6.9.3 citron was compiled against, -# causing at AppImage runtime: -# libQt6Core.so.6: version 'Qt_6.9' not found -# -# CITRON_QT_PATH = QT_TARGET_PATH from CMake = the Qt6 prefix directory (e.g. -# .../cpmcache/qt-bin/6.9.3/gcc_64). quick-sharun uses QT_LOCATION as the -# prefix and looks for plugins at $QT_LOCATION/plugins — that's exactly where -# aqt installs them. -if [ -n "${CITRON_QT_PATH:-}" ] && [ -z "${QT_LOCATION:-}" ]; then - export QT_LOCATION="${CITRON_QT_PATH}" -fi - -# Fix for "libQt6Core.so.6: version 'Qt_6.9' not found" at AppImage runtime. -# -# Root cause: quick-sharun COPIES each CPM Qt plugin into AppDir/shared/lib/ -# before running ldd on the copy. The plugins carry a $ORIGIN-relative -# DT_RUNPATH (e.g. "$ORIGIN/../../../lib") that is correct when the plugin is -# in $QT_LOCATION/plugins/platforms/, but resolves to a nonsense path once -# $ORIGIN becomes AppDir/shared/lib/. ldd then falls through to the system -# linker search path and resolves libQt6Core.so.6 to the SYSTEM Qt (e.g. -# Qt 6.4.2 on Ubuntu 24.04). That system copy is staged into the AppImage as -# AppDir/shared/lib/libQt6Core.so.6. At runtime the CPM Qt 6.9 plugins -# require the Qt_6.9 version symbol — absent from the system 6.4 core — and -# the AppImage aborts with the version error. -# -# Fix: prepend CPM Qt's lib dir to LD_LIBRARY_PATH before calling quick-sharun. -# LD_LIBRARY_PATH has higher priority than DT_RUNPATH, so every ldd invocation -# inside lib4bin now finds CPM libQt6Core.so.6 (and all other Qt libs) -# regardless of broken $ORIGIN resolution. The citron binary itself is -# unaffected: it carries DT_RPATH (set by _patch_binary_rpaths via -# patchelf --force-rpath), which has higher priority than LD_LIBRARY_PATH. -if [ -n "${CITRON_QT_PATH:-}" ]; then - export LD_LIBRARY_PATH="${CITRON_QT_PATH}/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" -fi - - -# Use the vendored quick-sharun.sh committed alongside this script rather than -# downloading from pkgforge HEAD. The vendored copy has appimagetool pinned to -# 0.3.2 and sharun pinned to 1.0.0 (both already in the file); all DEPLOY_* -# overrides and cleanup rules in this script were calibrated against that -# specific version. Pulling HEAD risks silent regressions if pkgforge changes -# DEPLOY_* defaults or the AppDir layout between releases. -_qs_src="$(cd "$(dirname "$0")" && pwd)/quick-sharun.sh" -if [ ! -f "${_qs_src}" ]; then - printf 'ERROR: vendored quick-sharun.sh not found at %s\n' "${_qs_src}" >&2 - printf 'Commit quick-sharun.sh to the repo alongside this script.\n' >&2 - exit 1 -fi -cp "${_qs_src}" ./quick-sharun -chmod +x ./quick-sharun - -# Bundle binary + optional runtime libs. -# -# gamemode and libpulse are dlopen'd by cubeb/citron at runtime rather than -# linked at build time, so quick-sharun's automatic dependency scan won't -# find them — they must be passed explicitly if present. -# -# Locate them via `ldconfig -p` rather than a hardcoded /usr/lib/*.so* glob: -# on multiarch systems (Debian/Ubuntu) these libs live under -# /usr/lib//, not /usr/lib/ directly. A glob that matches nothing is -# passed to quick-sharun as a literal non-existent path by POSIX sh (no -# nullglob), which quick-sharun then treats as a hard requirement and aborts. -# Neither lib is guaranteed to be present (e.g. pure PipeWire setups have no -# libpulse.so), so both are best-effort. -EXTRA_LIBS="" - -GAMEMODE_LIB="$(ldconfig -p 2>/dev/null | awk '/libgamemode\.so/ {print $NF; exit}')" -if [ -n "$GAMEMODE_LIB" ]; then - EXTRA_LIBS="$EXTRA_LIBS $GAMEMODE_LIB" -fi - -PULSE_LIB="$(ldconfig -p 2>/dev/null | awk '/libpulse\.so/ {print $NF; exit}')" -if [ -n "$PULSE_LIB" ]; then - EXTRA_LIBS="$EXTRA_LIBS $PULSE_LIB" -fi - -# shellcheck disable=SC2086 -./quick-sharun "${DESTDIR}/usr/bin/citron"* $EXTRA_LIBS - -# Qt translations: do NOT copy manually here. -# quick-sharun's --make-appimage (lib4bin) already copies -# /usr/share/qt6/translations → AppDir/lib/qt6/translations/ automatically -# when it detects Qt. Adding a second copy from install-root would produce -# two separate translation trees (AppDir/usr/share/qt6/ and -# AppDir/lib/qt6/) wasting ~100 MB in the AppImage with no benefit. -# The qt.conf written below points Translations to the one quick-sharun stages. - -# ── Post-dep-scan cleanup ──────────────────────────────────────────────────── -# Belt-and-suspenders: remove any bloat that slipped past the DEPLOY_* flags -# or the STRACE_MODE=0 suppression (e.g. from ldd transitive chains we don't -# fully control, or from a future quick-sharun version that overrides our flags -# differently). This runs BEFORE --make-appimage so lib4bin never sees them. -# -# Pattern Why excluded -# libvulkan_*.so Mesa Vulkan ICD drivers: system-specific, huge. -# SHARUN_ALLOW_SYS_VK_ICD=1 uses host GPU driver. -# libLLVM*.so Mesa LLVM backend (137 MB): only needed by Mesa ICDs. -# vulkan/icd.d/* ICD manifest JSONs: useless without the ICD .so files. -# vulkan/implicit_layer.d/* Vulkan validation / overlay layers: dev tooling only. -# pipewire-*/ PipeWire backend plugins: resolved from host at runtime. -# spa-*/ PipeWire SPA plugins: same. -# alsa-lib/ ALSA plugins: same. -# gconv/ glibc character-set converters: citron never calls iconv. -_appdir="${PWD}/AppDir" - -# Explicitly stage ld-linux into AppDir/shared/lib/ — lib4bin's AppRun.lib -# prints "Interpreter not found!" and aborts if this file is absent. -# -# sharun -g (called by quick-sharun) is supposed to copy it, but it reads the -# interpreter path from AppDir/shared/bin/citron AFTER _patch_away_usr_lib_dir -# has run sed -i on the binary. GNU sed on an ELF can silently corrupt the -# section that encodes PT_INTERP, causing sharun -g to find no interpreter and -# skip the copy. We do it here from the original install-root binary (never -# touched by sed) so it is guaranteed present regardless of sharun -g's outcome. -_interp=$(patchelf --print-interpreter "${DESTDIR}/usr/bin/citron" 2>/dev/null || true) -if [ -z "${_interp}" ]; then - # patchelf fallback: parse readelf output - _interp=$(readelf -l "${DESTDIR}/usr/bin/citron" 2>/dev/null \ - | awk '/\[Requesting program interpreter:/{gsub(/[][]/,""); print $NF}') -fi -if [ -n "${_interp}" ] && [ -f "${_interp}" ]; then - mkdir -p "${_appdir}/shared/lib" - cp -L "${_interp}" "${_appdir}/shared/lib/${_interp##*/}" - printf 'Staged interpreter: %s\n' "${_interp##*/}" -else - printf 'WARNING: could not determine PT_INTERP of citron; AppImage may fail with "Interpreter not found!"\n' >&2 -fi - -find "${_appdir}" -name 'libvulkan_*.so' -delete 2>/dev/null || true -find "${_appdir}" -name 'libLLVM*.so*' -delete 2>/dev/null || true -find "${_appdir}" -path '*/vulkan/icd.d/*' -delete 2>/dev/null || true -find "${_appdir}" -path '*/vulkan/implicit_layer.d/*' -delete 2>/dev/null || true -find "${_appdir}" -path '*/pipewire-*/*' -type f -delete 2>/dev/null || true -find "${_appdir}" -path '*/spa-*/*' -type f -delete 2>/dev/null || true -find "${_appdir}" -path '*/alsa-lib/*' -type f -delete 2>/dev/null || true -find "${_appdir}" -path '*/gconv/*' -type f -delete 2>/dev/null || true - -# Remove system XCB libs that duplicate the CPM xcb-build copies. -# -# citron's DT_RPATH (set by _patch_binary_rpaths) points to xcb-build before -# any system path. lib4bin strips those RPATHs, so at runtime all resolution -# goes through lib.path. The Qt XCB plugin (libqxcb.so from CPM Qt) pulls in -# system libxcb.so.1, libXau.so.6, and libXdmcp.so.6 via its own ldd chain, -# staging them flat in AppDir/lib/. The xcb-build copies of the same three -# libraries are already in AppDir at the absolute-mirrored path; keeping both -# is dead weight and violates the "CPM over system" policy. -# -# -maxdepth 1 on AppDir/lib/ hits only the flat system copies; the xcb-build -# copies are nested deep under home/thayne/…/xcb-build/lib/ and are untouched. -find "${_appdir}/lib" -maxdepth 1 -name 'libxcb.so*' -delete 2>/dev/null || true -find "${_appdir}/lib" -maxdepth 1 -name 'libXau.so*' -delete 2>/dev/null || true -find "${_appdir}/lib" -maxdepth 1 -name 'libXdmcp.so*' -delete 2>/dev/null || true - -# Qt Multimedia's FFmpeg backend — belt-and-suspenders removal. -# -# citron is built with -DCITRON_USE_BUNDLED_FFMPEG=ON (static FFmpeg for game -# media decoding) and -DCITRON_USE_QT_MULTIMEDIA=OFF (set in build-citron-linux.sh). -# With CITRON_USE_QT_MULTIMEDIA=OFF these files are never staged because Qt -# Multimedia is not linked. These find/delete lines remain as a safety net in -# case the flag is ever changed or if a future Qt version starts staging the -# shared FFmpeg unconditionally. -# -# Background: Qt Multimedia pulls in a SEPARATE shared FFmpeg -# (libavcodec.so.61 ~80 MB, libavformat.so.61, libavutil.so.59, libswresample, -# libswscale) via libffmpegmediaplugin.so. This is distinct from citron's own -# statically compiled FFmpeg and adds ~100 MB to the AppImage for no benefit -# (all camera/video recording code is Qt5-only and permanently dead under Qt6). -find "${_appdir}" -name 'libffmpegmediaplugin.so' -delete 2>/dev/null || true -find "${_appdir}" -name 'libQt6FFmpegStub-*.so*' -delete 2>/dev/null || true -find "${_appdir}" -name 'libavcodec.so*' -delete 2>/dev/null || true -find "${_appdir}" -name 'libavformat.so*' -delete 2>/dev/null || true -find "${_appdir}" -name 'libavutil.so*' -delete 2>/dev/null || true -find "${_appdir}" -name 'libswresample.so*' -delete 2>/dev/null || true -find "${_appdir}" -name 'libswscale.so*' -delete 2>/dev/null || true - - - -# Write a portable qt.conf next to libQt6Core.so.6 in shared/lib/. -# -# CPM Qt's libQt6Core.so.6 has a baked-in INSTALL_PREFIX of -# /home//cpmcache/qt-bin/6.9.x/gcc_64. Without an explicit qt.conf, -# Qt uses that absolute prefix to locate its plugins and translations at -# runtime — which works on the build machine (the CPM directory exists on -# disk, outside the AppImage) but silently breaks on every other machine. -# -# A qt.conf placed next to libQt6Core.so.6 (AppDir/shared/lib/) overrides -# the embedded prefix. All [Paths] entries are relative to Prefix; Prefix -# itself is relative to the qt.conf file's directory. -# -# qt.conf at: AppDir/shared/lib/qt.conf -# Prefix = .. → AppDir/shared/ -# Plugins = lib → AppDir/shared/lib/ (flat dir where lib4bin stages all plugins) -# Translations → AppDir/usr/share/qt6/translations -# -# This also clobbers any qt.conf from the CPM installation that lib4bin or -# strace may have staged here, ensuring absolute build-machine paths cannot -# escape into the AppImage. -mkdir -p ./AppDir/shared/lib -cat > ./AppDir/shared/lib/qt.conf << 'QTCONF_EOF' -[Paths] -Prefix = .. -Plugins = lib -Imports = lib/qt6/qml -Qml2Imports = lib/qt6/qml -Translations = lib/qt6/translations -QTCONF_EOF - -# Rename app in desktop file if building a devel/nightly AppImage -if [ "${DEVEL:-false}" = 'true' ]; then - sed -i 's|^Name=citron$|Name=citron nightly|' ./AppDir/*.desktop 2>/dev/null || true -fi - -# Allow system Vulkan ICD to override the bundled one at runtime, and write -# PGO profile data next to the running AppImage on exit (matches the old -# linuxdeploy $APPIMAGE_DIR convention). $APPIMAGE is exported by sharun's -# AppRun at runtime and points to the AppImage file's own path. -{ - printf 'SHARUN_ALLOW_SYS_VK_ICD=1\n' - printf 'LLVM_PROFILE_FILE=$(dirname "$APPIMAGE")/default-%%p.profraw\n' -} > ./AppDir/.env - -# Build the AppImage -./quick-sharun --make-appimage - -mkdir -p "${OUTPATH}" -mv -v ./*.AppImage "${OUTPATH}/" 2>/dev/null || true -mv -v ./*.AppImage.* "${OUTPATH}/" 2>/dev/null || true - -# Pack the portable tar.zst alongside the AppImage — the "+ tar.zst" half -# promised by this script's header comment, previously unimplemented. -# -# AppDir at this point is the exact same fully-debloated tree that was just -# squashed into the AppImage above (every DEPLOY_*/STRACE_MODE suppression and -# the post-dep-scan find/-delete block above already ran before -# --make-appimage), so this is a complete, AppRun-runnable, install-free -# alternative to the AppImage — not a partial copy. (AppDir/usr/ is not used -# by this layout — translations land under AppDir/lib/qt6/, see comment above — -# so packing only "usr", as the pre-quick-sharun pipeline did, would produce a -# near-empty archive here.) -# -# AppDir is then removed. It has no further purpose once packed, and leaving -# it sitting in OUTPATH means anything that uploads/copies OUTPATH wholesale -# (e.g. a CI artifact upload step) drags along every loose unpacked library a -# second time alongside the AppImage that already contains them compressed. -if [ -d ./AppDir ]; then - if ! command -v zstd >/dev/null 2>&1; then - printf 'ERROR: zstd is not installed — cannot produce %s.tar.zst\n' "${OUTNAME_BASE}" >&2 - printf 'Install zstd (e.g. apt-get install zstd) and re-run.\n' >&2 - exit 1 - fi - tar -c --zstd -f "${OUTPATH}/${OUTNAME_BASE}.tar.zst" -C ./AppDir . - rm -rf ./AppDir -fi - -# Clean up build-tool byproducts that land in this same directory (OUTPATH), -# not just AppDir. build-citron-linux.sh sets OUTPATH to the same working -# directory this script cd's into and downloads tooling into, so anything -# these tools write to cwd ends up sitting next to the final artifacts unless -# removed explicitly: -# -# quick-sharun — the tool itself, copied to ./quick-sharun near the top of -# this script (see the vendoring comment above). It has no -# purpose after packaging finishes. -# appinfo — a debug/metadata text file written to cwd by the -# underlying appimagetool binary during --make-appimage. -# Not generated by quick-sharun.sh itself, not meant for -# redistribution — internal build debug output only. -rm -f ./quick-sharun ./appinfo - -echo "Artifacts in: ${OUTPATH}" diff --git a/AppImageBuilder/quick-sharun.sh b/AppImageBuilder/quick-sharun.sh deleted file mode 100644 index 3a9fe62075..0000000000 --- a/AppImageBuilder/quick-sharun.sh +++ /dev/null @@ -1,4196 +0,0 @@ -#!/bin/sh - -# wrapper script for sharun that simplifies deployment to simple one liners -# Will try to detect and force deployment of GTK, QT, OpenGL, etc -# You can also force their deployment by setting the respective env variables -# for example set DEPLOY_OPENGL=1 to force opengl to be deployed - -# Set ADD_HOOKS var to deploy the several hooks of this repository -# Example: ADD_HOOKS="self-updater.hook:fix-namespaces.hook" ./quick-sharun.sh -# Using the hooks automatically downloads a generic AppRun if no AppRun is present - -# Set DESKTOP and ICON to the path of top level .desktop and icon to deploy them - -set -e - -if [ "$QUICK_SHARUN_DEBUG" = 1 ]; then - set -x -fi - -APPIMAGE_ARCH=$(uname -m) -ARCH=${ARCH:-$APPIMAGE_ARCH} -TMPDIR=${TMPDIR:-/tmp} -APPDIR=${APPDIR:-$PWD/AppDir} -APPENV=$APPDIR/.env -DIRICON=$APPDIR/.DirIcon -DST_LIB_DIR=$APPDIR/lib -DST_BIN_DIR=$APPDIR/bin -SHARUN_BIN_DIR=$APPDIR/shared/bin -MAIN_BIN=${MAIN_BIN##*/} - -SHARUN_LINK=${SHARUN_LINK:-https://github.com/pkgforge-dev/sharun/releases/download/1.0.0/sharun-$APPIMAGE_ARCH} -ONELF_LINK=${ONELF_LINK:-https://github.com/QaidVoid/onelf/releases/latest/download/onelf-$APPIMAGE_ARCH-linux} -HOOKSRC=${HOOKSRC:-https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/refs/heads/main/useful-tools/hooks} -LD_PRELOAD_OPEN=${LD_PRELOAD_OPEN:-https://github.com/VHSgunzo/pathmap.git} - -OUTPATH=${OUTPATH:-$PWD} -DWARFS_COMP="${DWARFS_COMP:-zstd:level=22 -S26 -B6}" -OPTIMIZE_LAUNCH=${OPTIMIZE_LAUNCH:-0} - -APPIMAGETOOL_LINK=${APPIMAGETOOL_LINK:-https://github.com/pkgforge-dev/appimagetool/releases/download/0.3.2/appimagetool-$APPIMAGE_ARCH-linux} -APPIMAGETOOL=${APPIMAGETOOL:-$TMPDIR/appimagetool} - -ANYLINUX_LIB=${ANYLINUX_LIB:-1} -ANYLINUX_LIB_SOURCE=${ANYLINUX_LIB_SOURCE:-https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/refs/heads/main/useful-tools/lib/anylinux.c} -GTK_CLASS_FIX=${GTK_CLASS_FIX:-0} -GTK_CLASS_FIX_SOURCE=${GTK_CLASS_FIX_SOURCE:-https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/refs/heads/main/useful-tools/lib/gtk-class-fix.c} - -DEPLOY_DATADIR=${DEPLOY_DATADIR:-1} -DEPLOY_LOCALE=${DEPLOY_LOCALE:-1} -DEBLOAT_LOCALE=${DEBLOAT_LOCALE:-1} -LOCALE_DIR=${LOCALE_DIR:-/usr/share/locale} - -STRACE_MODE=${STRACE_MODE:-1} -STRACE_TIME=${STRACE_TIME:-5} - -DEPENDENCIES=" - awk - cc - cp - find - grep - ldd - mv - patchelf - rm - sleep - strings - tr -" - -# libraries whose dependencies should not be collected via ldd -# we skip libqgtk3.so by default to prevent deploying GTK in Qt apps -# Qt works fine by doing this, the libqgtk3.so plugin even works with alpine -# linux gtk3 without issue, if the plugin fails to load Qt does not crash either -QUICK_SHARUN_SKIP_DEPS_FOR=" - $QUICK_SHARUN_SKIP_DEPS_FOR - libqgtk3.so -" - -# prevent Qt from dlopening libqtgtk3.so via strace mode -export QT_QPA_PLATFORMTHEME=${QT_QPA_PLATFORMTHEME:-fusion} - -# check if the _tmp_* vars have not be declared already -# likely to happen if this script run more than once -PATH_MAPPING_SCRIPT=$DST_BIN_DIR/01-path-mapping-hardcoded.hook - -if [ -f "$PATH_MAPPING_SCRIPT" ]; then - while IFS= read -r line; do - case "$line" in - _tmp_*) eval "$line";; - esac - done < "$PATH_MAPPING_SCRIPT" -fi - -regex='A-Za-z0-9_=-' -_tmp_bin="${_tmp_bin:-$(tr -dc "$regex" < /dev/urandom | head -c 3)}" -_tmp_lib="${_tmp_lib:-$(tr -dc "$regex" < /dev/urandom | head -c 3)}" -_tmp_share="${_tmp_share:-$(tr -dc "$regex" < /dev/urandom | head -c 5)}" - -if [ "$DEPLOY_PYTHON" = 1 ]; then - DEPLOY_SYS_PYTHON=1 -fi - -if [ "$DEPLOY_SYS_PYTHON" = 1 ]; then - if [ "$DEBLOAT_PYTHON" = 0 ]; then - DEBLOAT_SYS_PYTHON=${DEBLOAT_SYS_PYTHON:-0} - fi - DEBLOAT_SYS_PYTHON=${DEBLOAT_SYS_PYTHON:-1} -fi - -# github actions doesn't set USER and XDG_RUNTIME_DIR -# causing some apps crash when running xvfb-run -export USER="${LOGNAME:-${USER:-${USERNAME:-yomama}}}" -export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/tmp}" - -# apps often need this to work -export $(dbus-launch 2>/dev/null || echo 'NO_DBUS=1') - -# CI containers often run as root which prevents -# web apps from running with LD_DEBUG strace mode -export ELECTRON_DISABLE_SANDBOX=1 -export WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS=1 -export QTWEBENGINE_DISABLE_SANDBOX=1 - -_echo() { - printf '\033[1;92m%s\033[0m\n' " $*" -} - -_err_msg(){ - >&2 printf '\033[1;31m%s\033[0m\n' " $*" -} - -_is_cmd() { - for cmd do - command -v "$cmd" 1>/dev/null || return 1 - done - return 0 -} - -_is_elf() { - head -c 4 "$1" 2>/dev/null | grep -qa 'ELF' -} - -_is_script() { - shebang=$(head -c 2 "$1" 2>/dev/null) - [ "$shebang" = '#!' ] -} - -_is_so() { - case "${1##*/}" in - *.so|*.so.[0-9]*) - return 0 - ;; - esac - return 1 -} - -_lib4bin_ldd_libs() { - ldd "$1" 2>/dev/null | awk '/=>/{print $3}' | sort -u -} - -_download() { - if _is_cmd wget; then - DOWNLOAD_CMD="wget" - set -- -qO "$@" - elif _is_cmd curl; then - DOWNLOAD_CMD="curl" - set -- -Lso "$@" - else - _err_msg "ERROR: we need wget or curl to download $1" - exit 1 - fi - COUNT=0 - while [ "$COUNT" -lt 5 ]; do - if "$DOWNLOAD_CMD" "$@"; then - return 0 - fi - _err_msg "Download failed! Trying again..." - COUNT=$((COUNT + 1)) - sleep 5 - done - _err_msg "ERROR: Failed to download 5 times!" - return 1 -} - -_help_msg() { - cat <<-EOF - USAGE: ${0##*/} /path/to/binaries_and_libraries - - DESCRIPTION: - POSIX shell script wrapper for sharun that simplifies the deployment - of AppImages to simple oneliners. It automates detection and deployment of common - libraries such as GTK, Qt, OpenGL, Vulkan, Pipewire, GStreamer, etc. - - Features: - - Automatic detection and forced deployment of libraries. - - Support for environment-based configuration to force deployment, e.g., DEPLOY_OPENGL=1 - - Deployment of app-specific hooks, desktop entries, icons, locale data and more. - - Automatic patching of hardcoded paths in binaries and libraries. - - OPTIONS / ENVIRONMENT VARIABLES: - ADD_HOOKS List of hooks (colon-separated) to deploy with the application. - DESKTOP Path or URL to a .desktop file to include. - ICON Path or URL to an icon file to include. - OUTPUT_APPIMAGE Set to 1 to turn the deployed AppDir into an AppImage. - DEPLOY_QT Set to 1 to force deployment of Qt. Will determine to deploy - QtWebEngine and Qml as well, these can be controlled with - DEPLOY_QT_WEB_ENGINE and DEPLOY_QML. Set to 1 enable, 0 disable - Set QT_DIR if the system Qt directory in LIB_DIR has a different name. - DEPLOY_SDL Set to 1 to force deployment of SDL. - DEPLOY_GTK Set to 1 to force deployment of GTK. - DEPLOY_GDK Set to 1 to force deployment of gdk-pixbuf. - DEPLOY_GLYCIN Set to 1 to force deployment of Glycin. - DEPLOY_OPENGL Set to 1 to force deployment of OpenGL. - DEPLOY_VULKAN Set to 1 to force deployment of Vulkan. - DEPLOY_IMAGEMAGICK Set to 1 to force deployment of ImageMagick. - DEPLOY_LIBHEIF Set to 1 to force deployment of libheif. - DEPLOY_GEGL Set to 1 to force deployment of GEGL. - DEPLOY_BABL Set to 1 to force deployment of babl. - DEPLOY_P11KIT Set to 1 to force deployment of p11-kit. - DEPLOY_PIPEWIRE Set to 1 to force deployment of Pipewire. - DEPLOY_PULSE Set to 1 to force deployment of pulseaudio. - DEPLOY_GSTREAMER Set to 1 to force deployment of GStreamer. By default - several gstreamer plugins are removed, set DEPLOY_GSTREAMER_ALL=1 - if you can to deploy ALL Gstreamer plugins. (Very bloated). - DEPLOY_LOCALE Set to 1 to deploy locale data. - DEPLOY_PYTHON Set to 1 to deploy system Python. Will remove all pycache - files, set DEBLOAT_PYTHON to 0 to prevent this. - - LIB_DIR Set source library directory if autodetection fails. - NO_STRIP Disable stripping binaries and libraries if set. - APPDIR Destination AppDir (default: ./AppDir). - ANYLINUX_LIB Preloads a library that unsets environment variables known to - cause problems to child processes. Set to 0 to disable. - Additionally you can set ANYLINUX_DO_NOT_LOAD_LIBS to a - list of colon separated libraries to prevent from being - dlopened, the entries support simple globbing, example: - export ANYLINUX_DO_NOT_LOAD_LIBS='libpipewire-0.3.so*' - Useful for applications that will try to dlopen several - optional dependencies that you do not want to include. - - ALWAYS_SOFTWARE Set to 1 to enable. Sets several env variables to make - applications use software rendering, use this option when - you do not want hardware acceleration. - Will fail if the application makes use of mesa during deployment. - - PATH_MAPPING Configures and preloads pathmap. - Set this variable if the application is hardcoded to look - into /usr and similar locations, example: - export PATH_MAPPING=' - /usr/lib/myapp_libs:\${SHARUN_DIR}/lib/myapp_libs - /etc/myapp.conf:\${SHARUN_DIR}/etc/myapp.conf - ' - \${SHARUN_DIR} here must NOT expand! - The braces in the variable are mandatory! - - NOTE: - Several of these options get turned on automatically based on what is being deployed. - - EXAMPLES: - DEPLOY_OPENGL=1 ./quick-sharun.sh /path/to/myapp - DESKTOP=/path/to/app.desktop ICON=/path/to/icon.png ./quick-sharun.sh /path/to/myapp - ADD_HOOKS="self-updater.hook:fix-namespaces.hook" ./quick-sharun.sh /path/to/myapp - - SEE ALSO: - sharun (https://github.com/VHSgunzo/sharun) - pathmap (https://github.com/VHSgunzo/pathmap) - EOF - exit 1 -} - -_get_icon() { - if [ -f "$DIRICON" ]; then - return 0 - fi - - icon_name=$(awk -F'=' '/^Icon=/{print $2; exit}' "$DESKTOP_ENTRY") - icon_name=${icon_name##*/} - - if [ "$ICON" = "DUMMY" ]; then - if [ -z "$icon_name" ]; then - _err_msg "ERROR: Cannot get icon name from $DESKTOP_ENTRY" - _err_msg "Make sure it contains a valid 'Icon=' key!" - exit 1 - fi - _echo "* Adding dummy $icon_name icon to $APPDIR..." - :> "$APPDIR"/"$icon_name".png - :> "$DIRICON" - elif [ -f "$ICON" ]; then - _echo "* Adding $ICON to $APPDIR..." - cp -v "$ICON" "$APPDIR" - cp -v "$ICON" "$DIRICON" - elif echo "$ICON" | grep -q 'http'; then - _echo "* Downloading $ICON to $APPDIR..." - dst=$APPDIR/${ICON##*/} - _download "$dst" "$ICON" - cp -v "$dst" "$DIRICON" - elif [ -n "$ICON" ]; then - _err_msg "$ICON is NOT a valid path!" - exit 1 - fi - - if [ ! -f "$DIRICON" ]; then - # try the first top level .png or .svg before searching - set -- "$APPDIR"/*.png "$APPDIR"/*.svg - for i do - if [ -f "$i" ]; then - cp -v "$i" "$DIRICON" - return 0 - fi - done - set -- - - # Now search deeper - if [ -n "$icon_name" ]; then - sizes='256x256 512x512 192x192 128x128 scalable' - for s in $sizes; do - set -- "$@" "$APPDIR"/share/icons/hicolor/"$s"/apps/"$icon_name"* - done - for s in $sizes; do - set -- "$@" /usr/share/icons/hicolor/"$s"/apps/"$icon_name"* - done - for i do - if [ -f "$i" ]; then - case "$i" in - *.png|*.svg) - cp -v "$i" "$APPDIR" - cp -v "$i" "$DIRICON" - break - ;; - esac - fi - done - set -- - fi - fi - - if [ ! -f "$DIRICON" ]; then - _err_msg "ERROR: Missing '$DIRICON'!" - _err_msg "Could not find icon listed in $DESKTOP_ENTRY either" - _err_msg "Set ICON env variable to the location/url of the icon" - exit 1 - fi -} - -_sanity_check() { - for d in $DEPENDENCIES; do - if ! _is_cmd "$d"; then - _err_msg "ERROR: Missing dependency '$d'!" - exit 1 - fi - done - - if ! mkdir -p "$APPDIR"/share "$DST_LIB_DIR" "$DST_BIN_DIR" "$SHARUN_BIN_DIR"; then - _err_msg "ERROR: Cannot create '$APPDIR' directory!" - exit 1 - fi - - if [ -n "$PATH_MAPPING" ] && ! echo "$PATH_MAPPING" | grep -q 'SHARUN_DIR'; then - _err_msg 'ERROR: PATH_MAPPING must contain unexpanded ${SHARUN_DIR} variable' - _err_msg 'Example:' - _err_msg "'PATH_MAPPING=/etc:\${SHARUN_DIR}/etc'" - _err_msg 'NOTE: The braces in the variable are needed!' - exit 1 - fi - - if [ "$STRACE_MODE" = 1 ]; then - if _is_cmd xvfb-run; then - XVFB_CMD="xvfb-run -a --" - else - _err_msg "WARNING: xvfb-run was not detected on the system" - _err_msg "xvfb-run is used in strace mode to provide a display" - _err_msg "for apps to run to find dlopened libraries." - _err_msg "GUI apps will not run without display! We cannot check for dlopened libs!" - XVFB_CMD="" - sleep 5 - fi - fi - - unset LIB32 - if [ -z "$LIB_DIR" ]; then - if [ -d "/usr/lib/$APPIMAGE_ARCH-linux-gnu" ]; then - LIB_DIR="/usr/lib/$APPIMAGE_ARCH-linux-gnu" - elif [ -d "/usr/lib" ]; then - LIB_DIR="/usr/lib" - else - _err_msg "ERROR: there is no /usr/lib directory in this system" - _err_msg "set the LIB_DIR variable to where you have libraries" - exit 1 - fi - elif [ "$LIB_DIR" = /usr/lib32 ] || [ "$LIB_DIR" = /usr/lib/i386-linux-gnu ]; then - LIB32=1 - fi - - set -- lib - if [ "$LIB32" = 1 ]; then - DST_LIB_DIR=$APPDIR/lib32 - _err_msg "WARNING: 32bit deployment is experimental!" - set -- "$@" lib32 - fi - - for d do - [ -L "$APPDIR"/shared/"$d" ] || rm -rf "$APPDIR"/shared/"$d" - [ -L "$APPDIR"/shared/"$d" ] || ln -sf ../"$d" "$APPDIR"/shared/"$d" - done -} - -# do a basic test to make sure at least the application is not totally broken -# like when libraries are missing symbols and similar stuff -_test_appimage() { - if [ -z "$1" ]; then - _err_msg "ERROR: Missing application to run!" - exit 1 - elif ! _is_cmd xvfb-run; then - _err_msg "ERROR: --test requires 'xvfb-run'!" - exit 1 - fi - - APP=$1 - shift - - _echo "------------------------------------------------------------" - _echo "Testing '$APP'..." - _echo "------------------------------------------------------------" - - # Allow host vulkan for vulkan-swrast since there is no GPU in the CI - export SHARUN_ALLOW_SYS_VKICD=1 - - # since there is no fuse available in CI and userns are also broken - # the appimage may not run if it is bigger than 400 MiB due to a restriction - # in the uruntime, so we will have to always force it to extract and run - export APPIMAGE_TARGET_DIR="$PWD"/_test-app - export APPIMAGE_EXTRACT_AND_RUN=1 - - set -m - xvfb-run -a -- "$APP" "$@" & - pid=$! - set +m - - # let the app run for 12 seconds, if it exits early it means something is wrong - COUNT=0 - while kill -0 $pid 2>/dev/null && [ "$COUNT" -lt 12 ]; do - sleep 1 - COUNT=$((COUNT + 1)) - done - - set +e - if kill -0 $pid 2>/dev/null; then - _echo "------------------------------------------------------------" - _echo "Test went OK." - _echo "------------------------------------------------------------" - kill -TERM -$pid 2>/dev/null || : - sleep 1 - kill -KILL -$pid 2>/dev/null || : - exit 0 - else - # process exited before timeout, something went wrong. - wait $pid - status=$? - _err_msg "------------------------------------------------------------" - _err_msg "ERROR: '$APP' failed in ${COUNT} seconds with code $status" - _err_msg "------------------------------------------------------------" - # wait 20 seconds before failing, this way for example if we have a Ci run - # for x86_64 and aarch64, if one fails it does not instantly stop the other - # and people are left wondering if the problem affects both matrix or just one - sleep 20 - exit 1 - fi -} - -# if full test is not possible lets at least check some possible issues -_simple_test_appimage() { - log="$TMPDIR"/simple-test.log - APP=$1 - shift - - _echo "------------------------------------------------------------" - _echo "Doing simple test '$APP'..." - _echo "------------------------------------------------------------" - - set -m - "$APP" "$@" 2>"$log" & - pid=$! - set +m - - sleep 7 - kill -TERM -$pid 2>/dev/null || : - sleep 1 - kill -KILL -$pid 2>/dev/null || : - - test="$(cat "$log")" - case "$test" in - *'symbol lookup error'*|\ - *'error while loading shared libraries'*) - >&2 echo "$test" - _err_msg "------------------------------------------------------------" - _err_msg "ERROR: '$APP' failed simple test!" - _err_msg "------------------------------------------------------------" - sleep 20 - exit 1 - ;; - esac - - _echo "------------------------------------------------------------" - _echo "Test went OK." - _echo "------------------------------------------------------------" - exit 0 -} - -# POSIX shell doesn't support arrays we use awk to save it into a variable -# then with 'eval set -- $var' we add it to the positional array -# see https://unix.stackexchange.com/questions/421158/how-to-use-pseudo-arrays-in-posix-shell-script -_save_array() { - LC_ALL=C awk -v q="'" ' - BEGIN{ - for (i=1; i/dev/null || true -} - -# skip non executable binaries and .node binaries -# these are actually libraries and cannot be wrapped with sharun -_is_deployable_binary() { - if [ -x "$1" ]; then - case "$1" in - *.node) :;; - *) return 0;; - esac - fi - return 1 -} - -_is_bun_binary() { - grep -aq -m 1 '__bun_' "$1" -} - -_is_pyinstaller_binary() { - grep -aq -m 1 'pydata' "$1" -} - -_determine_what_to_deploy() { - for bin do - # ignore flags - case "$bin" in - --) break ;; - -*) continue;; - esac - - if [ ! -e "$bin" ]; then - _err_msg "'$bin' is NOT present!" - exit 1 - fi - - # if the argument is a directory save it to later it copy it - if [ -d "$bin" ]; then - ADD_DIR=" - $ADD_DIR - $bin - " - elif [ -x "$bin" ]; then - # some apps may dlopen pulseaudio instead of linking directly - if grep -aoq -m 1 'libpulse.so' "$bin"; then - DEPLOY_PULSE=${DEPLOY_PULSE:-1} - fi - if grep -aoq -m 1 'disable-gpu-sandbox' "$bin" \ - && grep -aoq -m 1 'no-zygote-sandbox' "$bin"; then - DEPLOY_ELECTRON=${DEPLOY_ELECTRON:-1} - ELECTRON_BIN=$(readlink -f "$bin") - fi - fi - - # check if what we are doing to deploy is not fucking broken - if _is_elf "$bin" && ldd "$bin" | grep "not found"; then - _err_msg "$bin is missing libraries! Aborting..." - exit 1 - fi - - NEEDED_LIBS="$(ldd "$bin" 2>/dev/null | awk '{print $3}') $NEEDED_LIBS" - - # bin may be a shared library, in that case add it as well - case "$bin" in - *.so*) NEEDED_LIBS="$bin $NEEDED_LIBS";; - esac - - # check linked libraries and enable each mode accordingly - for lib in $NEEDED_LIBS; do - case "$lib" in - *libQt5Core.so*) - DEPLOY_QT=${DEPLOY_QT:-1} - QT_DIR=${QT_DIR:-qt5} - ;; - *libQt6Core.so*) - DEPLOY_QT=${DEPLOY_QT:-1} - QT_DIR=${QT_DIR:-qt6} - ;; - *libQt*Qml*.so*) - DEPLOY_QML=${DEPLOY_QML:-1} - ;; - *libQt*WebEngineCore.so*) - DEPLOY_QT_WEB_ENGINE=${DEPLOY_QT_WEB_ENGINE:-1} - DEPLOY_ELECTRON=${DEPLOY_ELECTRON:-1} - ;; - *libgtk-x11-*.so*) - DEPLOY_GTK=${DEPLOY_GTK:-1} - GTK_DIR=gtk-2.0 - ;; - *libgtk-3*.so*) - DEPLOY_GTK=${DEPLOY_GTK:-1} - GTK_DIR=gtk-3.0 - ;; - *libgtk-4*.so*) - DEPLOY_GTK=${DEPLOY_GTK:-1} - GTK_DIR=gtk-4.0 - ;; - *libgdk_pixbuf*.so*) - DEPLOY_GDK=${DEPLOY_GDK:-1} - ;; - *libglycin*.so*) - # glycin-ng needs no special handling - # it works out of the box - case " $NEEDED_LIBS " in - *"libglycin_ng.so"*) - DEPLOY_GLYCIN=0 - continue - ;; - *) - DEPLOY_GLYCIN=${DEPLOY_GLYCIN:-1} - GTK_CLASS_FIX=1 - GNOME_GLYCIN=1 - ;; - esac - ;; - *libwebkit*gtk-*.so*) - DEPLOY_WEBKIT2GTK=${DEPLOY_WEBKIT2GTK:-1} - _webkit_dir=${lib##*/} # get basename - _webkit_dir=${_webkit_dir#lib} # strip lib - _webkit_dir=${_webkit_dir%.so*} # strip .so - WEBKIT2GTK_DIR=${WEBKIT2GTK_DIR:-${lib%/*}/$_webkit_dir} - ;; - *libsoup-*.so*) - DEPLOY_GLIB_NETWORKING=${DEPLOY_GLIB_NETWORKING:-1} - ;; - *libSDL*.so*) - DEPLOY_SDL=${DEPLOY_SDL:-1} - ;; - *libflutter*linux*.so*) - DEPLOY_FLUTTER=${DEPLOY_FLUTTER:-1} - FLUTTER_LIB=$lib - ;; - *libpipewire*.so*) - DEPLOY_PIPEWIRE=${DEPLOY_PIPEWIRE:-1} - ;; - *libgstreamer*.so*) - DEPLOY_GSTREAMER=${DEPLOY_GSTREAMER:-1} - ;; - *libMagick*.so*) - DEPLOY_IMAGEMAGICK=${DEPLOY_IMAGEMAGICK:-1} - ;; - *libImlib2.so*) - DEPLOY_IMLIB2=${DEPLOY_IMLIB2:-1} - ;; - *libgegl*.so*) - DEPLOY_GEGL=${DEPLOY_GEGL:-1} - ;; - *libbabl*.so*) - DEPLOY_BABL=${DEPLOY_BABL:-1} - ;; - *libheif.so*) - DEPLOY_LIBHEIF=${DEPLOY_LIBHEIF:-1} - ;; - *libgs.so*) - DEPLOY_GHOSTSCRIPT=${DEPLOY_GHOSTSCRIPT:-1} - ;; - *libp11-kit.so*) - DEPLOY_P11KIT=${DEPLOY_P11KIT:-1} - ;; - # libc.so.6 is glibc only, musl does not have it - *libc.so.6* |\ - *libdl.so* |\ - *libpthread.so*|\ - *ld-linux*.so* |\ - *librt.so* ) - DEPLOY_GLIBC=${DEPLOY_GLIBC:-1} - ;; - esac - done - done - - if [ "$DEPLOY_QT" = 1 ] && [ -z "$QT_DIR" ]; then - _err_msg - _err_msg "WARNING: Qt deployment was forced but we do not know" - _err_msg "what version of Qt needs to be deployed!" - _err_msg "Defaulting to Qt6, if you do not want that set" - _err_msg "QT_DIR to the name of the Qt dir in $LIB_DIR" - _err_msg - QT_DIR=qt6 - fi - - if [ "$DEPLOY_GTK" = 1 ] && [ -z "$GTK_DIR" ]; then - _err_msg - _err_msg "WARNING: GTK deployment was forced but we do not know" - _err_msg "what version of GTK needs to be deployed!" - _err_msg "Defaulting to gtk-3.0, if you do not want that set" - _err_msg "GTK_DIR to the name of the gtk dir in $LIB_DIR" - _err_msg - GTK_DIR=gtk-3.0 - fi -} - -_make_deployment_array() { - if [ "$DEPLOY_GLIBC" = 1 ]; then - _echo "* Deploying glibc" - # ancient glibc libs still needed for compat (nvidia drivers, old binaries, etc.) - set -- "$@" \ - "$LIB_DIR"/libpthread.so* \ - "$LIB_DIR"/libdl.so* \ - "$LIB_DIR"/librt.so* \ - "$LIB_DIR"/libm.so* \ - "$LIB_DIR"/libutil.so* \ - "$LIB_DIR"/libresolv.so* \ - "$LIB_DIR"/libnsl.so* - # nss libs, not all apps need this but it is very hard to determine this - set -- "$@" \ - "$LIB_DIR"/libnss_dns.so* \ - "$LIB_DIR"/libnss_files.so* \ - "$LIB_DIR"/libnss_resolve.so* \ - "$LIB_DIR"/libnss_mymachines.so* - # gconv is always deployed, removing it only saves ~30 KiB - # in the final appimage size and not worth the hassle - # It also causes hard to spot issues when needed and not present - # - # https://github.com/pkgforge-dev/Dolphin-emu-AppImage/issues/20 - # https://github.com/pkgforge-dev/Anylinux-AppImages/pull/410 - set -- "$@" \ - "$LIB_DIR"/gconv/UTF*.so* \ - "$LIB_DIR"/gconv/ANSI*.so* \ - "$LIB_DIR"/gconv/CP*.so* \ - "$LIB_DIR"/gconv/LATIN*.so* \ - "$LIB_DIR"/gconv/UNICODE*.so* \ - "$LIB_DIR"/gconv/ISO8859*.so* - fi - if [ "$ALWAYS_SOFTWARE" = 1 ]; then - DEPLOY_OPENGL=0 - DEPLOY_VULKAN=0 - echo 'GSK_RENDERER=cairo' >> "$APPENV" - echo 'GDK_DISABLE=gl,vulkan' >> "$APPENV" - echo 'GDK_GL=disable' >> "$APPENV" - echo 'QT_QUICK_BACKEND=software' >> "$APPENV" - export GSK_RENDERER=cairo - export GDK_DISABLE=gl,vulkan - export GDK_GL=disable - export QT_QUICK_BACKEND=software - - ANYLINUX_DO_NOT_LOAD_LIBS="libgallium-*:libvulkan*:libGLX_mesa.so*:libGLX_indirect.so*${ANYLINUX_DO_NOT_LOAD_LIBS:+:$ANYLINUX_DO_NOT_LOAD_LIBS}" - fi - if [ "$DEPLOY_PYTHON" = 1 ]; then - _echo "* Deploying system python" - fi - if [ "$DEPLOY_QT" = 1 ]; then - DEPLOY_OPENGL=${DEPLOY_OPENGL:-1} - DEPLOY_COMMON_LIBS=${DEPLOY_COMMON_LIBS:-1} - - _echo "* Deploying $QT_DIR" - - if [ -d "$QT_LOCATION" ]; then - plugindir="$QT_LOCATION"/plugins - else - # some distros have a qt dir rather than qt6 or qt5 dir - if [ ! -d "$LIB_DIR"/"$QT_DIR" ]; then - QT_DIR=qt - fi - plugindir="$LIB_DIR"/"$QT_DIR"/plugins - fi - - for lib in $NEEDED_LIBS; do - case "$lib" in - *libQt*Gui.so*) - set -- "$@" \ - "$plugindir"/imageformats/* \ - "$plugindir"/iconengines/* \ - "$plugindir"/styles/* \ - "$plugindir"/platform*/* \ - "$plugindir"/wayland-*/* \ - "$plugindir"/xcbglintegrations/* - ;; - *libQt*Network.so*) - set -- "$@" \ - "$plugindir"/tls/* \ - "$plugindir"/bearer/* - ;; - *libQt*Sql.so*) - set -- "$@" "$plugindir"/sqldrivers/* - ;; - *libQt*Multimedia.so*) - set -- "$@" "$plugindir"/multimedia/* - ;; - *libQt*PrintSupport*) - set -- "$@" "$plugindir"/printsupport/* - ;; - *libQt*Positioning.so*) - set -- "$@" "$plugindir"/position/* - ;; - esac - done - - if [ "$DEPLOY_QT_WEB_ENGINE" = 1 ]; then - if ! enginebin=$(find "${QT_LOCATION:-$LIB_DIR}" -type f \ - -name 'QtWebEngineProcess' -print -quit 2>/dev/null); then - _err_msg "Cannot find QtWebEngineProcess!" - exit 1 - fi - set -- "$@" "$enginebin" - fi - - if [ "$DEPLOY_QML" = 1 ]; then - _echo "* Deploying qml" - qmldir="${QT_LOCATION:-$LIB_DIR/$QT_DIR}"/qml - ADD_DIR=" - $ADD_DIR - $qmldir - " - fi - fi - if [ "$DEPLOY_GTK" = 1 ]; then - _echo "* Deploying $GTK_DIR" - DEPLOY_GDK=${DEPLOY_GDK:-1} - DEPLOY_COMMON_LIBS=${DEPLOY_COMMON_LIBS:-1} - set -- "$@" \ - "$LIB_DIR"/"$GTK_DIR"/*/immodules/* \ - "$LIB_DIR"/gvfs/libgvfscommon.so \ - "$LIB_DIR"/gio/modules/libgvfsdbus.so \ - "$LIB_DIR"/gio/modules/libdconfsettings.so - - case "$GTK_DIR" in - *4*) - DEPLOY_OPENGL=${DEPLOY_OPENGL:-1} - echo 'GSETTINGS_BACKEND=keyfile' >> "$APPENV" - ;; - esac - - if [ "$DEPLOY_WEBKIT2GTK" = 1 ]; then - _echo "* Deploying webkit2gtk" - DEPLOY_OPENGL=${DEPLOY_OPENGL:-1} - DEPLOY_P11KIT=${DEPLOY_P11KIT:-1} - DEPLOY_GLIB_NETWORKING=${DEPLOY_GLIB_NETWORKING:-1} - set -- "$@" "$LIB_DIR"/libnss_mdns*minimal.so* - if b=$(command -v bwrap); then set -- "$@" "$b"; fi - if b=$(command -v xdg-dbus-proxy); then set -- "$@" "$b"; fi - if [ ! -d "$WEBKIT2GTK_DIR" ]; then - _err_msg "Unable to find $WEBKIT2GTK_DIR directory" - _err_msg "Please set the WEBKIT2GTK_DIR variable to its location" - exit 1 - fi - ADD_DIR=" - $ADD_DIR - $WEBKIT2GTK_DIR - " - fi - - if [ "$DEPLOY_GLIB_NETWORKING" = 1 ]; then - _echo "* Deploying Glib-Netwroking" - DEPLOY_P11KIT=${DEPLOY_P11KIT:-1} - set -- "$@" \ - "$LIB_DIR"/gio/modules/libgiognutls.so \ - "$LIB_DIR"/gio/modules/libgiolibproxy.so \ - "$LIB_DIR"/gio/modules/libgiognomeproxy.so - fi - - if [ "$DEPLOY_SYS_PYTHON" = 1 ]; then - set -- "$@" "$LIB_DIR"/libgirepository*.so* - fi - fi - if [ "$DEPLOY_GDK" = 1 ]; then - _echo "* Deploying gdk-pixbuf" - gdkdir="$(echo "$LIB_DIR"/gdk-pixbuf-*/*/loaders)" - - set -- "$@" "$gdkdir"/*svg*.so* - for lib in $NEEDED_LIBS; do - case "$lib" in - *libjxl.so*) set -- "$@" "$gdkdir"/*jxl*.so* ;; - *libavif.so*) set -- "$@" "$gdkdir"/*avif*.so*;; - *libheif.so*) set -- "$@" "$gdkdir"/*heif*.so*;; - esac - done - fi - if [ "$DEPLOY_SDL" = 1 ]; then - _echo "* Deploying SDL" - DEPLOY_PULSE=${DEPLOY_PULSE:-1} - DEPLOY_COMMON_LIBS=${DEPLOY_COMMON_LIBS:-1} - set -- "$@" \ - "$LIB_DIR"/libSDL*.so* \ - "$LIB_DIR"/libudev.so* \ - "$LIB_DIR"/libusb-1*.so* \ - "$LIB_DIR"/libdecor*.so* - fi - if [ "$DEPLOY_GLYCIN" = 1 ]; then - _echo "* Deploying GNOME glycin" - set -- "$@" "$LIB_DIR"/glycin-loaders/*/* - if b=$(command -v bwrap); then set -- "$@" "$b"; fi - fi - if [ "$DEPLOY_FLUTTER" = 1 ]; then - DEPLOY_COMMON_LIBS=${DEPLOY_COMMON_LIBS:-1} - DEPLOY_OPENGL=${DEPLOY_OPENGL:-1} - fi - if [ "$DEPLOY_ELECTRON" = 1 ] || [ "$DEPLOY_CHROMIUM" = 1 ]; then - _echo "* Deploying electron/chromium" - DEPLOY_COMMON_LIBS=${DEPLOY_COMMON_LIBS:-1} - DEPLOY_P11KIT=${DEPLOY_P11KIT:-1} - DEPLOY_OPENGL=${DEPLOY_OPENGL:-1} - DEPLOY_VULKAN=${DEPLOY_VULKAN:-1} - set -- "$@" \ - "$LIB_DIR"/libnss*.so* \ - "$LIB_DIR"/libsoftokn3.so* \ - "$LIB_DIR"/libfreeblpriv3.so* \ - "$LIB_DIR"/libnss_mdns*_minimal.so* - # electron has a resources directory that may have binaries - d="${ELECTRON_BIN%/*}"/resources - if [ -d "$d" ]; then - for f in $(find "$d" -type f ! -name '*.so*'); do - if _is_deployable_binary "$f"; then - set -- "$@" "$f" - fi - done - fi - # electron bundled libs always need to load first - # for example libcef.so may need to read a icudtl.dat next to it - echo 'SHARUN_EXTRA_LIBRARY_PATH=${SHARUN_DIR}/bin:${SHARUN_EXTRA_LIBRARY_PATH}' >> "$APPENV" - fi - if [ "$DEPLOY_OPENGL" = 1 ] || [ "$DEPLOY_VULKAN" = 1 ]; then - DEPLOY_COMMON_LIBS=${DEPLOY_COMMON_LIBS:-1} - set -- "$@" \ - "$LIB_DIR"/dri/* \ - "$LIB_DIR"/gbm/* \ - "$LIB_DIR"/vdpau/* \ - "$LIB_DIR"/libgbm.so* \ - "$LIB_DIR"/libvdpau.so* \ - "$LIB_DIR"/libpci.so* \ - "$LIB_DIR"/libva.so* \ - "$LIB_DIR"/libva-*.so* \ - "$LIB_DIR"/libdrm*.so* \ - "$LIB_DIR"/libxcb-dri*.so* \ - "$LIB_DIR"/libxcb-glx.so* \ - "$LIB_DIR"/libgallium*.so* - if [ "$DEPLOY_OPENGL" = 1 ]; then - _echo "* Deploying OpenGL" - set -- "$@" \ - "$LIB_DIR"/libEGL*.so* \ - "$LIB_DIR"/libGLX*.so* \ - "$LIB_DIR"/libGL.so* \ - "$LIB_DIR"/libOpenGL.so* \ - "$LIB_DIR"/libGLESv2.so* - fi - if [ "$DEPLOY_VULKAN" = 1 ]; then - _echo "* Deploying vulkan" - set -- "$@" \ - "$LIB_DIR"/libvulkan*.so* \ - "$LIB_DIR"/libVkLayer*.so* - ADD_HOOKS="${ADD_HOOKS:+$ADD_HOOKS:}vulkan-check.hook" - fi - fi - if [ "$DEPLOY_PIPEWIRE" = 1 ]; then - _echo "* Deploying pipewire" - DEPLOY_PULSE=${DEPLOY_PULSE:-1} - set -- "$@" \ - "$LIB_DIR"/pipewire-*/* \ - "$LIB_DIR"/spa-*/* \ - "$LIB_DIR"/spa-*/*/* \ - "$LIB_DIR"/alsa-lib/*pipewire*.so* - fi - if [ "$DEPLOY_PULSE" = 1 ]; then - set -- "$@" \ - "$LIB_DIR"/libpulse.so* \ - "$LIB_DIR"/alsa-lib/libasound*pulse*.so* - fi - # deploy a minimal set of alsa plugins, we don't have a deploy alsa mode - # it would make no sense to ship an application with only support for alsa - # - # but we end up with libasound.so bundled as a transitive dependency of - # pipewire or pulseaudio which means people on alsa only setups will run - # into issues if the alsa plugins are missing - if [ "$DEPLOY_PULSE" = 1 ] || [ "$DEPLOY_PIPEWIRE" = 1 ]; then - set -- "$@" \ - "$LIB_DIR"/alsa-lib/libasound*pcm_upmix.so* \ - "$LIB_DIR"/alsa-lib/libasound*pcm_vdownmix.so* \ - "$LIB_DIR"/alsa-lib/libasound*pcm_speex.so* \ - "$LIB_DIR"/alsa-lib/libasound*pcm_usb_stream.so* \ - "$LIB_DIR"/alsa-lib/libasound*rate_speexrate*.so* \ - "$LIB_DIR"/alsa-lib/libasound*rate_samplerate*.so* - fi - if [ "$DEPLOY_GSTREAMER_ALL" = 1 ] || [ "$DEPLOY_GSTREAMER" = 1 ]; then - GST_DIR=$(echo "$LIB_DIR"/gstreamer-*) - if [ "$DEPLOY_GSTREAMER_ALL" = 1 ]; then - _echo "* Deploying all gstreamer" - elif [ "$DEPLOY_GSTREAMER" = 1 ]; then - _echo "* Deploying minimal gstreamer" - - # we need to delete the plugins on the host because copying - # the libs to a different place and pointing to that dir - # does not work, all the plugins still end up being deployed - - # check we have write access to the directory - # and make sure we are in a container since someone could - # run this script in their personal PC with elevated rights... - if [ -w "$GST_DIR" ] && [ -n "$CI" ]; then - # gstreamer has a lot of plugins - # remove the following since they pull a lot of deps: - - # has a dependency to libicudata (30 MIB lib) - rm -f "$GST_DIR"/*gstladspa* - # gstx265 has a dependency to libx265, massive library - rm -f "$GST_DIR"/*gstx265* - # gstsvt-hevc video encoder, rarely needed - rm -f "$GST_DIR"/*gstsvthevcenc* - # Apparently this is only useful in windows? - rm -f "$GST_DIR"/*gstopenmpt* - # Never heard of this format before lol - rm -f "$GST_DIR"/*gstopenexr* - # used to scan barcodes - rm -f "$GST_DIR"/*gstzxing* - # dvd playback - rm -f "$GST_DIR"/*gstdvdspu* - rm -f "$GST_DIR"/*gstresindvd* - # only needed for recording with some capture card - rm -f "$GST_DIR"/*gstdecklink* - # mpeg2 video encoder - rm -f "$GST_DIR"/*gstmpeg2enc* - # wtf is this? - rm -f "$GST_DIR"/*gstmplex* - # gstreamer already has png and svg plugins - # so it is unlikely that we also need gdkpixbuf - rm -f "$GST_DIR"/*libgstgdkpixbuf* - # Apprently this can be used by some video players - # but I cannot find a single one that uses it lol - rm -f "$GST_DIR"/*libgstcairo* - # text to speech - rm -f "$GST_DIR"/*libgstfestival* - # gstvulkan pulls vulkan, remove unless vulkan is deployed - if [ "$DEPLOY_VULKAN" != 1 ]; then - rm -f "$GST_DIR"/*gstvulkan* - fi - # also make sure to delete gstreamer plugins - # that are missing libraries, otherwise they - # will load libraries from the host and crash - for plugin in "$GST_DIR"/*.so*; do - if ldd "$plugin" | grep -q 'not found'; then - rm -f "$plugin" - fi - done - fi - fi - set -- "$@" \ - "$GST_DIR"/*.so* \ - "$GST_DIR"/gst*helper \ - "$GST_DIR"/gst*scanner - # On ubuntu and alpine the gstreamer binaries are on a different dir - if [ ! -f "$GST_DIR"/gst-plugin-scanner ]; then - gst_bin_path=$(find /usr/lib* -type f \ - -name 'gst-plugin-scanner' -print -quit) - gst_bin_dir=${gst_bin_path%/*} - set -- "$@" \ - "$gst_bin_dir"/gst*scanner \ - "$gst_bin_dir"/gst*helper - fi - fi - if [ "$DEPLOY_IMAGEMAGICK" = 1 ]; then - _echo "* Deploying ImageMagick" - set -- "$@" "$LIB_DIR"/libMagick*.so* - if b=$(command -v magick); then set -- "$@" "$b"; fi - if b=$(command -v convert); then set -- "$@" "$b"; fi - # imagemagick optionally requires potrace to convert png to svg - if b=$(command -v potrace); then set -- "$@" "$b"; fi - - magickdir=$(echo "$LIB_DIR"/ImageMagick*) - ADD_DIR=" - $ADD_DIR - $magickdir - " - fi - if [ "$DEPLOY_IMLIB2" = 1 ]; then - _echo "* Deploying Imlib2" - set -- "$@" \ - "$LIB_DIR"/libImlib2.so* \ - "$LIB_DIR"/imlib2/filters/* \ - "$LIB_DIR"/imlib2/loaders/* - fi - if [ "$DEPLOY_SYS_PYTHON" = 1 ]; then - if b=$(command -v python); then set -- "$@" "$b"* - elif b=$(command -v python3); then set -- "$@" "$b"* - fi - - d=$(set -- "$LIB_DIR"/python* && echo "$1") - if [ ! -d "$d" ]; then - _err_msg "ERROR: Cannot find python installation in $LIB_DIR" - exit 1 - fi - mkdir -p "$DST_LIB_DIR" - cp -r "$d" "$DST_LIB_DIR" - ( - if [ "$DEBLOAT_SYS_PYTHON" = 1 ]; then - cd "$DST_LIB_DIR"/"${d##*/}" - find ./ -type f -name '*.a' -delete || : - for f in $(find ./ -type f -name '*.pyc' -print); do - case "$f" in - */"$MAIN_BIN"*) :;; - *) [ ! -f "$f" ] || rm -f "$f";; - esac - done - fi - ) - _fix_cpython_ldconfig_mess - fi - if [ "$DEPLOY_GEGL" = 1 ]; then - _echo "* Deploying gegl" - set -- "$@" "$LIB_DIR"/gegl-*/* - if b=$(command -v gegl); then set -- "$@" "$b"; fi - if b=$(command -v gegl-imgcmp); then set -- "$@" "$b"; fi - fi - if [ "$DEPLOY_BABL" = 1 ]; then - _echo "* Deploying babl" - set -- "$@" "$LIB_DIR"/babl-*/* - fi - if [ "$DEPLOY_LIBHEIF" = 1 ]; then - _echo "* Deploying libheif" - - if [ -d "$LIB_DIR"/libheif/plugins ]; then - heifdir="$LIB_DIR"/libheif/plugins - elif [ -d "$LIB_DIR"/libheif ]; then - heifdir="$LIB_DIR"/libheif - fi - - # do not add the ffmpeg plugin by default - # only do it if libavcodec is already required - for p in "$heifdir"/*; do - case "$p" in - *ffmpeg*) continue;; - *) set -- "$@" "$p";; - esac - done - for lib in $NEEDED_LIBS; do - case "$lib" in - *libavcodec.so*) set -- "$@" "$heifdir"/*ffmpeg*.so*;; - esac - done - fi - if [ "$DEPLOY_GHOSTSCRIPT" = 1 ]; then - _echo "* Deploying ghostscript" - set -- "$@" "$LIB_DIR"/libgs.so* - if b=$(command -v gs); then set -- "$@" "$b"; fi - fi - if [ "$DEPLOY_P11KIT" = 1 ]; then - _echo "* Deploying p11kit" - set -- "$@" "$LIB_DIR"/pkcs11/* - fi - if [ "$DEPLOY_DOTNET" = 1 ]; then - _echo "* Deploying dotnet" - DEPLOY_COMMON_LIBS=${DEPLOY_COMMON_LIBS:-1} - if [ -z "$DOTNET_DIR" ]; then - if [ -d /usr/lib/dotnet ]; then - DOTNET_DIR=/usr/lib/dotnet - elif [ -d /usr/share/dotnet ]; then - DOTNET_DIR=/usr/share/dotnet - fi - fi - if [ ! -d "$DOTNET_DIR" ]; then - _err_msg "Cannot find dotnet installation, searched for" - _err_msg "/usr/lib/dotnet and /usr/share/dotnet" - _err_msg "Set DOTNET_DIR variable if it is somewhere else" - exit 1 - fi - set -- "$@" \ - "$(command -v dotnet)" \ - $(find "$DOTNET_DIR"/shared -type f -name '*.so*' -print) - cp -r "$DOTNET_DIR"/shared "$DST_BIN_DIR" - cp -r "$DOTNET_DIR"/host "$DST_BIN_DIR" - echo 'DOTNET_ROOT=${SHARUN_DIR}/bin' >> "$APPENV" - fi - # these are needed by several toolkits - if [ "$DEPLOY_COMMON_LIBS" = 1 ]; then - set -- "$@" \ - "$LIB_DIR"/libXi.so* \ - "$LIB_DIR"/libXcursor.so* \ - "$LIB_DIR"/libXtst.so* \ - "$LIB_DIR"/libxcb-ewmh.so* \ - "$LIB_DIR"/libxcb-icccm.so* \ - "$LIB_DIR"/libxkbcommon.so* \ - "$LIB_DIR"/libxkbcommon-x11.so* \ - "$LIB_DIR"/libXext.so* \ - "$LIB_DIR"/libXfixes.so* \ - "$LIB_DIR"/libXinerama.so* \ - "$LIB_DIR"/libXrandr.so* \ - "$LIB_DIR"/libXss.so* \ - "$LIB_DIR"/libX11-xcb.so* \ - "$LIB_DIR"/libwayland-egl.so* \ - "$LIB_DIR"/libwayland-cursor.so* \ - "$LIB_DIR"/libwayland-client.so* - fi - - # also pass all the files in the directories to add to lib4bin - # so we deploy any possible library and binary in the directories - # later on the binaries in lib will be wrapped with sharun - if [ -n "$ADD_DIR" ]; then - _echo "* Deploying directories:" - while read -r d; do - if [ -d "$d" ]; then - _echo " - $d" - for f in \ - "$d"/* \ - "$d"/*/* \ - "$d"/*/*/* \ - "$d"/*/*/*/* \ - "$d"/*/*/*/*/* \ - "$d"/*/*/*/*/*/*; do - - if [ ! -f "$f" ]; then - continue - fi - - case "$f" in - *.so*) - set -- "$@" "$f" - ;; - *) - if _is_deployable_binary "$f"; then - set -- "$@" "$f" - fi - ;; - esac - done - fi - done <<-EOF - $ADD_DIR - EOF - fi - - TO_DEPLOY_ARRAY=$(_save_array "$@") -} - -_get_sharun() { - if [ -x "$APPDIR"/sharun ]; then - return 0 - fi - _echo "Downloading sharun..." - _download "$APPDIR"/sharun "$SHARUN_LINK" - if _is_elf "$APPDIR"/sharun; then - chmod +x "$APPDIR"/sharun - else - _err_msg "ERROR: What was downloaded is not sharun!" - _err_msg "This is usually caused by network issues" - exit 1 - fi -} - -_deploy_libs() { - # now merge the deployment array - eval set -- "$TO_DEPLOY_ARRAY" "$@" - _lib4bin_main "$@" -} - -# compute destination path in DST_LIB_DIR from a source file path -_lib4bin_get_lib_dst_dir() { - p=$(readlink -f "${1%/*}" | sed \ - -e "s|^$LIB_DIR||" \ - -e 's|^/usr||' \ - -e 's|^/opt||' \ - -e 's|^/lib64||' \ - -e 's|^/lib32||' \ - -e 's|^/lib||' \ - -e 's|^/[^/]*-linux-gnu||' - ) - echo "$DST_LIB_DIR"/"$p" -} - -# collect ldd library dependencies -_lib4bin_collect_ldd() { - libs="" - for b do - [ -f "$b" ] || continue - _is_elf "$b" || continue - - skip="" - # do not deploy dependencies for libs in QUICK_SHARUN_SKIP_DEPS_FOR - while read -r d; do - if [ "$d" = "${b##*/}" ]; then - skip=1 - break - fi - done <<-EOF - $QUICK_SHARUN_SKIP_DEPS_FOR - EOF - - if [ -z "$skip" ]; then - libs=$(printf '%s\n%s' "$libs" "$(_lib4bin_ldd_libs "$b")") - fi - - if _is_so "$b"; then - libs=$(printf '%s\n%s' "$libs" "$b") - fi - done - echo "$libs" | sort -u | sed '/^$/d' -} - -# collect dlopen libraries via LD_DEBUG=libs -# STRACE_BINARY=space/newline-separated binary names to trace (default: all) -_lib4bin_collect_strace() { - [ "$STRACE_MODE" = 1 ] || return 0 - - libs="" - for b do - [ -f "$b" ] || continue - _is_elf "$b" || continue - [ -x "$b" ] || continue - if _is_so "$b"; then - continue - fi - - if [ -n "$STRACE_BINARY" ]; then - match="" - flags="" - for strace_bin in $STRACE_BINARY; do - if [ "$strace_bin" = "${b##*/}" ]; then - match=1 - flags=$STRACE_FLAGS - break - fi - done - [ -n "$match" ] || continue - fi - - dlopened=$TMPDIR/libs.$$ - - _echo "STRACE: [$b] ..." - set -m - if [ -n "$XVFB_CMD" ]; then - $XVFB_CMD LD_DEBUG=libs "$b" $flags >/dev/null 2>"$dlopened" & - else - LD_DEBUG=libs "$b" $flags >/dev/null 2>"$dlopened" & - fi - pid=$! - set +m - - sleep "$STRACE_TIME" - kill -TERM -$pid 2>/dev/null || : - sleep 1 - kill -KILL -$pid 2>/dev/null || : - wait $pid 2>/dev/null || : - - out=$(awk '/calling init/{print $NF}' "$dlopened" | sed \ - -e '/nvidia/d' \ - -e '/libcuda/d' \ - -e '/lib-dynload/d' \ - -e '/_internal/d' - ) - rm -f "$dlopened" - [ -n "$out" ] || continue - libs=$(printf '%s\n%s' "$libs" "$out") - done - echo "$libs" | sort -u | sed '/^$/d' -} - -# deploy shared libraries to DST_LIB_DIR -_lib4bin_deploy_shared_libs() { - for l do - [ -f "$l" ] || continue - if ! _is_elf "$l"; then - _echo "SKIPPED: [$l] not shared object!" - continue - fi - - l_name=${l##*/} - - if [ -L "$l" ]; then - readlink_path=$(readlink -f "$l") || continue - readlink_path_name=${readlink_path##*/} - dst_dir=$(_lib4bin_get_lib_dst_dir "$readlink_path") - else - readlink_path_name="" - dst_dir=$(_lib4bin_get_lib_dst_dir "$l") - fi - - if [ -n "$readlink_path_name" ]; then - dst=$dst_dir/$readlink_path_name - else - dst=$dst_dir/$l_name - fi - - mkdir -p "$dst_dir" - [ -f "$dst" ] || cp -fv "$l" "$dst" - - # symlink may land in a "wrong" subdir if it targets a relative dir - # example: libfltk.so.1.3.11 -> fltk1.3/libfltk.so.1.3.11 on archlinux - # but sharun adds all subdirs to --library-path, so nothing should break - # note: original lib4bin actually made broken symlinks when this happened - if [ -n "$readlink_path_name" ] \ - && [ "$l_name" != "$readlink_path_name" ]; then - ln -sf "$readlink_path_name" "$dst_dir"/"$l_name" - fi - done -} - -# deploy binaries, download sharun if needed -_lib4bin_deploy_binaries() { - seen="" - for b do - echo "$seen" | grep -Fxq "$b" && continue - seen=$(printf '%s\n%s' "$seen" "$b") - - [ -f "$b" ] || continue - - if _is_script "$b"; then - dst=$DST_BIN_DIR/${b##*/} - mkdir -p "$DST_BIN_DIR" - if [ ! -f "$dst" ]; then - cp -fv "$b" "$dst" - chmod 755 "$dst" - fi - for i in python bash sh ash zsh fish dash perl ruby go node; do - if grep -qo "^#!.*bin/$i" "$dst"; then - sed -i "1s|^#!.*bin/$i|#!/usr/bin/env $i|" "$dst" - break - fi - done - continue - fi - - _is_elf "$b" || continue - [ -x "$b" ] || continue - if _is_so "$b"; then - continue - fi - - [ -x "$APPDIR/sharun" ] || _get_sharun - - b_name=${b##*/} - - if [ -L "$b" ]; then - readlink_path=$(readlink -f "$b") || continue - readlink_path_name=${readlink_path##*/} - else - readlink_path_name="" - fi - - if [ -n "$readlink_path_name" ]; then - dst=$SHARUN_BIN_DIR/$readlink_path_name - else - dst=$SHARUN_BIN_DIR/$b_name - fi - - mkdir -p "$SHARUN_BIN_DIR" - if [ ! -f "$dst" ]; then - cp -fv "$b" "$dst" - chmod +x "$dst" - fi - - if [ -n "$readlink_path_name" ] \ - && [ "$b_name" != "$readlink_path_name" ]; then - ln -sf "$readlink_path_name" "$SHARUN_BIN_DIR"/"$b_name" - fi - - # hardlink in bin/ -> ../sharun - mkdir -p "$DST_BIN_DIR" && ( - cd "$DST_BIN_DIR" - ln -f ../sharun "$b_name" - if [ -n "$readlink_path_name" ] \ - && [ "$b_name" != "$readlink_path_name" ]; then - ln -f ../sharun "$readlink_path_name" - fi - ) || exit 1 - done -} - -_lib4bin_main() { - _echo "Collecting dependencies..." - ldd_libs=$(_lib4bin_collect_ldd "$@") - - strace_libs='' - if [ "$STRACE_MODE" = 1 ]; then - _echo "Collecting dlopen libraries via LD_DEBUG=libs..." - strace_libs="$(_lib4bin_collect_strace "$@")" - fi - - all_libs=$(printf '%s\n%s' "$ldd_libs" "$strace_libs" | sort -u | sed '/^$/d') - - _echo "Deploying shared libraries..." - _lib4bin_deploy_shared_libs $all_libs - - _echo "Deploying binaries..." - _lib4bin_deploy_binaries "$@" - - _echo "Generating lib.path..." - if [ -x "$APPDIR/sharun" ]; then - "$APPDIR/sharun" -g - else - _err_msg "ERROR: sharun binary not found at $APPDIR/sharun!" - exit 1 - fi -} - -_handle_bins_scripts() { - # check for gstreamer binaries these need to be in the gstreamer libdir - # since sharun will set the following vars to that location: - # GST_PLUGIN_PATH - # GST_PLUGIN_SYSTEM_PATH - # GST_PLUGIN_SYSTEM_PATH_1_0 - # GST_PLUGIN_SCANNER - set -- "$DST_LIB_DIR"/gstreamer-* - if [ -d "$1" ]; then - gstlibdir="$1" - set -- "$SHARUN_BIN_DIR"/gst-* - for bin do - if [ -f "$bin" ]; then - ln "$APPDIR"/sharun "$gstlibdir"/"${bin##*/}" - fi - done - fi - - if [ "$DEPLOY_QT_WEB_ENGINE" = 1 ]; then - src_res=/usr/share/$QT_DIR/resources - dst_res=$DST_LIB_DIR/$QT_DIR/resources - if [ -d "$src_res" ] && [ ! -d "$dst_res" ]; then - mkdir -p "${dst_res%/*}" - cp -r "$src_res" "$dst_res" - fi - fi - - # handle shell scripts - set -- "$DST_BIN_DIR"/* - for s do - if ! head -c 20 "$s" | grep -q '#!.*sh'; then - continue - fi - # some very very old distros do not have /usr/bin/env - # so it is better to always use #!/bin/sh shebang instead - sed -i -e 's|/usr/bin/env sh|/bin/sh|' "$s" - - # patch away hardcoded paths from dotnet scripts - if grep -q 'dotnet' "$s"; then - sed -i -e '/^#/!s|/usr|"$APPDIR"|g' "$s" - fi - done - -} - -_add_anylinux_lib() { - cfile=$APPDIR/.anylinux.c - target=$DST_LIB_DIR/anylinux.so - - if [ "$ANYLINUX_LIB" != 1 ]; then - return 0 - elif [ ! -f "$target" ]; then - _echo "* Building anylinux.so..." - _download "$cfile" "$ANYLINUX_LIB_SOURCE" - - set -- -shared -fPIC -O2 "$cfile" -o "$target" - if [ "$LIB32" = 1 ]; then - set -- -m32 "$@" - fi - cc "$@" - fi - - if ! grep -q 'anylinux.so' "$APPDIR"/.preload 2>/dev/null; then - echo "anylinux.so" >> "$APPDIR"/.preload - fi - - _echo "* anylinux.so successfully added!" -} - -_add_gtk_class_fix() { - cfile=$APPDIR/.gtk-class-fix.c - target=$DST_LIB_DIR/gtk-class-fix.so - - if [ "$GTK_CLASS_FIX" != 1 ]; then - return 0 - elif [ ! -f "$DESKTOP_ENTRY" ]; then - _err_msg "ERROR: Using GTK_CLASS_FIX requires a desktop entry in $APPDIR" - exit 1 - elif [ "$ANYLINUX_LIB" != 1 ]; then - _err_msg "ERROR: GTK_CLASS_FIX requires ANYLINUX_LIB=1" - exit 1 - fi - - _echo "* Building gtk-class-fix.so" - _download "$cfile" "$GTK_CLASS_FIX_SOURCE" - - set -- -shared -fPIC -O2 "$cfile" -o "$target" -ldl - if [ "$LIB32" = 1 ]; then - set -- -m32 "$@" - fi - cc "$@" - - # _check_window_class will make sure StartupWMClass is added to desktop entry - # for this to work in wayland, the class needs to have one dot in its name - if ! grep -q 'StartupWMClass=.*\..*' "$DESKTOP_ENTRY"; then - sed -i -e 's/\(StartupWMClass=.*\)/\1.anylinux/' "$DESKTOP_ENTRY" - fi - - class=$(awk -F'=| ' '/^StartupWMClass=/{print $2; exit}' "$DESKTOP_ENTRY") - - echo "GTK_WINDOW_CLASS=$class" >> "$APPDIR"/.env - echo "gtk-class-fix.so" >> "$APPDIR"/.preload - _echo "* gtk-class-fix.so successfully added!" -} - -_check_always_software() { - if [ "$ALWAYS_SOFTWARE" != 1 ]; then - return 0 - fi - set -- "$DST_LIB_DIR"/libgallium-*.so* - if [ -f "$1" ]; then - _err_msg "ALWAYS_SOFTWARE was enabled but mesa was deployed!" - _err_msg "Likely this application needs hardware acceleration." - _err_msg "Do not use this option or find a way to make sure" - _err_msg "the application does not dlopen mesa when running!" - exit 1 - fi -} - -_add_p11kit_cert_hook() { - cert_check=$DST_BIN_DIR/01-check-ca-certs.hook - if [ -f "$cert_check" ]; then - return 0 - fi - - cat <<-'EOF' > "$cert_check" - #!/bin/sh - - _possible_certs=' - /etc/ssl/certs/ca-certificates.crt - /etc/pki/tls/cert.pem - /etc/pki/tls/cacert.pem - /etc/ssl/cert.pem - /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem - /var/lib/ca-certificates/ca-bundle.pem - ' - - for c in $_possible_certs; do - if [ -f "$c" ]; then - break - fi - done - - if [ -f "$c" ]; then - # With p11kit we have to make a symlink in /tmp because the meme - # library does not check any of these variables set by sharun: - # - # REQUESTS_CA_BUNDLE - # CURL_CA_BUNDLE - # SSL_CERT_FILE - # - # So we had to patch it to a path in /tmp and now symlink to the - # found certificate at runtime... - _host_cert=/tmp/.___host-certs/ca-certificates.crt - if [ -d "$APPDIR"/lib/pkcs11 ] && [ ! -f "$_host_cert" ]; then - mkdir -p /tmp/.___host-certs || : - ln -sfn "$c" "$_host_cert" || : - fi - fi - EOF - chmod +x "$cert_check" -} - -_map_paths_ld_preload_open() { - # format new line entries in PATH_MAPPING into comma separated - # entries for sharun, pathmap accepts new lines in the variable - # but the .env library used by sharun does not - if [ -n "$PATH_MAPPING" ] && [ ! -f "$DST_LIB_DIR"/path-mapping.so ]; then - PATH_MAPPING=$(echo "$PATH_MAPPING" \ - | tr '\n' ',' | tr -d '[:space:]' | sed 's/,*$//; s/^,*//' - ) - - deps="git make" - if ! _is_cmd $deps; then - _err_msg "ERROR: Using PATH_MAPPING requires $deps" - exit 1 - fi - - _echo "* Building $LD_PRELOAD_OPEN..." - - rm -rf "$TMPDIR"/ld-preload-open - git clone "$LD_PRELOAD_OPEN" "$TMPDIR"/ld-preload-open && ( - cd "$TMPDIR"/ld-preload-open - make all - ) - - mv -v "$TMPDIR"/ld-preload-open/path-mapping.so "$DST_LIB_DIR" - echo "path-mapping.so" >> "$APPDIR"/.preload - echo "PATH_MAPPING=$PATH_MAPPING" >> "$APPENV" - _echo "* PATH_MAPPING successfully added!" - echo "" - fi -} - -_map_paths_binary_patch() { - if [ "$PATH_MAPPING_HARDCODED" = 1 ]; then - set -- "$SHARUN_BIN_DIR"/* - for bin do - _patch_away_usr_bin_dir "$bin" - _patch_away_usr_lib_dir "$bin" - _patch_away_usr_share_dir "$bin" - done - elif [ -n "$PATH_MAPPING_HARDCODED" ]; then - set -f - set -- $PATH_MAPPING_HARDCODED - set +f - _echo "* Patching files listed in PATH_MAPPING_HARDCODED..." - # only search for files to patch in the lib and bin dirs - path1=$SHARUN_BIN_DIR - path2=$DST_LIB_DIR - for f do - file=$(find -L "$path1"/ "$path2"/ -type f -name "$f") - if [ -n "$file" ]; then - for found in $file; do - _patch_away_usr_bin_dir "$found" || : - _patch_away_usr_lib_dir "$found" || : - _patch_away_usr_share_dir "$found" || : - done - else - _err_msg "ERROR: Could not find $f in $APPDIR" - exit 1 - fi - done - fi -} - -_deploy_datadir() { - if [ "$DEPLOY_DATADIR" = 1 ]; then - # find if there is a datadir that matches bundled binary name - set -- "$DST_BIN_DIR"/* - for bin do - if [ ! -f "$bin" ] || [ ! -x "$bin" ]; then - continue - fi - bin="${bin##*/}" - - # skip already handled cases - case "$bin" in - dotnet) continue;; - esac - - for datadir in /usr/local/share/* /usr/share/*; do - if echo "${datadir##*/}" | grep -qi "$bin"; then - _echo "* Adding datadir $datadir..." - # fallback to cp -r if cp -Lr fails - # due to broken symlinks in datadir - cp -Lr "$datadir" "$APPDIR"/share \ - || cp -r "$datadir" "$APPDIR"/share - break - fi - done - done - - set -- "$APPDIR"/*.desktop - - # Some apps have a datadir that does not match the binary name - # in that case we need to get it by reading the binary - if [ -f "$1" ]; then - - bin=$(awk -F'=| ' '/^Exec=/{print $2; exit}' "$1") - bin=${bin##*/} - possible_dirs=$( - strings "$SHARUN_BIN_DIR"/"$bin" \ - | grep -v '[;:,.(){}?<>*]' \ - | tr '/' '\n' - ) - - for datadir in $possible_dirs; do - # skip dirs not wanted or handled by sharun - case "$datadir" in - alsa |\ - applications|\ - awk |\ - bash |\ - clang |\ - dbus-1 |\ - defaults |\ - doc |\ - dotnet |\ - drirc.d |\ - et |\ - factory |\ - file |\ - fish |\ - fonts |\ - fontconfig |\ - ghostscript |\ - git |\ - glib-* |\ - glvnd |\ - glycin* |\ - gtk-doc |\ - gtksource* |\ - gvfs |\ - help |\ - i18n |\ - icons |\ - info |\ - java |\ - libdrm |\ - libthai |\ - locale |\ - man |\ - misc |\ - mime |\ - model |\ - p11-kit |\ - pipewire |\ - pixmaps |\ - qt |\ - qt4 |\ - qt5 |\ - qt6 |\ - qt7 |\ - ss |\ - swift |\ - systemd |\ - tabset |\ - terminfo |\ - themes |\ - vala |\ - vulkan |\ - wayland |\ - WebP |\ - X11 |\ - xcb |\ - zoneinfo |\ - zsh ) - continue - ;; - esac - - for path in /usr/local/share /usr/share; do - - src_datadir="$path"/"$datadir" - dst_datadir="$APPDIR"/share/"$datadir" - - if [ -d "$src_datadir" ] \ - && [ ! -d "$dst_datadir" ]; then - _echo "* Adding datadir $src_datadir..." - # cp can fail here if src_datadir contains broken links - if ! cp -Lr "$src_datadir" "$dst_datadir"; then - rm -rf "$dst_datadir" - cp -r "$src_datadir" "$dst_datadir" - fi - break - fi - done - done - fi - - # try to find and deploy a dbus service that matches .desktop - desktopname="${1%.desktop}" - desktopname="${desktopname##*/}" - dst_dbus_dir="$APPDIR"/share/dbus-1/services - for f in /usr/share/dbus-1/services/*; do - case "${f##*/}" in - *"$desktopname"*) - _echo "* Adding dbus service $f" - mkdir -p "$dst_dbus_dir" - cp -L "$f" "$dst_dbus_dir" - ;; - esac - done - sed -i -e 's|/usr/.*/||g' "$dst_dbus_dir"/* 2>/dev/null || : - fi -} - -_deploy_locale() { - if [ ! -d /usr/share/locale ]; then - _err_msg "This system does not have /usr/share/locale" - return 0 - fi - - set -- "$SHARUN_BIN_DIR"/* - for bin do - if grep -Eaoq -m 1 "/usr/share/locale" "$bin"; then - DEPLOY_LOCALE=1 - _patch_away_usr_share_dir "$bin" || true - fi - done - set -- - - if [ "$DEPLOY_LOCALE" = 1 ]; then - _echo "* Adding locales..." - cp -r "$LOCALE_DIR" "$APPDIR"/share - if [ "$DEBLOAT_LOCALE" = 1 ]; then - _echo "* Removing unneeded locales..." - for f in "$SHARUN_BIN_DIR"/* "$DST_BIN_DIR"/*; do - if [ -f "$f" ]; then - f=${f##*/} - set -- "$@" ! -name "*$f*" - fi - done - find "$APPDIR"/share/locale "$@" \( -type f -o -type l \) -delete - _remove_empty_dirs "$APPDIR"/share/locale - fi - echo "" - fi -} - -_get_desktop() { - DESKTOP_ENTRY=$(echo "$APPDIR"/*.desktop) - if [ -f "$DESKTOP_ENTRY" ]; then - return 0 - fi - - if [ "$DESKTOP" = "DUMMY" ]; then - if [ -z "$MAIN_BIN" ]; then - _err_msg "ERROR: DESKTOP=DUMMY needs MAIN_BIN to be set" - exit 1 - fi - _echo "* Adding dummy $MAIN_BIN desktop entry to $APPDIR..." - cat <<-EOF > "$APPDIR"/"$MAIN_BIN".desktop - [Desktop Entry] - Name=$MAIN_BIN - Exec=$MAIN_BIN - Comment=Dummy made by quick-sharun - Type=Application - Hidden=true - Categories=Utility - Icon=$MAIN_BIN - EOF - elif [ -f "$DESKTOP" ]; then - _echo "* Adding $DESKTOP to $APPDIR..." - cp -v "$DESKTOP" "$APPDIR" - elif echo "$DESKTOP" | grep -q 'http'; then - _echo "* Downloading $DESKTOP to $APPDIR..." - _download "$APPDIR"/"${DESKTOP##*/}" "$DESKTOP" - elif [ -n "$DESKTOP" ]; then - _err_msg "$DESKTOP is NOT a valid path!" - exit 1 - fi - - # make sure desktop entry ends with .desktop - if [ ! -f "$APPDIR"/*.desktop ] && [ -f "$APPDIR"/*.desktop* ]; then - filename="${DESKTOP##*/}" - mv "$APPDIR"/*.desktop* "$APPDIR"/"${filename%.desktop*}".desktop - fi - - DESKTOP_ENTRY=$(echo "$APPDIR"/*.desktop) - if [ ! -f "$DESKTOP_ENTRY" ]; then - _err_msg "ERROR: No top level .desktop file found in $APPDIR" - _err_msg "Note there cannot be more than one .desktop file in that location" - exit 1 - fi - - # strip DBusActivatable=true - # this leads to broken apps since no appimage manager installs dbus services - sed -i -e '/DBusActivatable=/d' "$DESKTOP_ENTRY" -} - -_check_window_class() { - set -- "$APPDIR"/*.desktop - - # do not bother if no desktop entry or class is declared already - if [ ! -f "$1" ] || grep -q 'StartupWMClass=' "$1"; then - return 0 - fi - - if [ -z "$STARTUPWMCLASS" ]; then - _err_msg "WARNING: '$1' is missing StartupWMClass!" - _err_msg "We will fix it using the name of the binary but this" - _err_msg "may be wrong so please add the correct value if so" - _err_msg "set STARTUPWMCLASS so I can set that instead" - bin="$(awk -F'=| ' '/^Exec=/{print $2; exit}' "$1")" - bin=${bin##*/} - if [ -z "$bin" ]; then - _err_msg "ERROR: Unable to determine name of binary" - exit 1 - fi - fi - - class=${STARTUPWMCLASS:-$bin} - sed -i -e "/\[Desktop Entry\]/a\StartupWMClass=$class" "$1" -} - -_add_bwrap_wrapper() { - cfile=$APPDIR/.anylinux-bwrap-wrapper.c - target=$DST_BIN_DIR/bwrap - realbwrap=$DST_BIN_DIR/bwrap.wrapped - - - if [ -f "$realbwrap" ]; then - return 0 # We wrapped it already - elif [ -f "$target" ]; then - # rename the real bwrap - mv "$target" "$realbwrap" - # rename the real binary as well - mv "$SHARUN_BIN_DIR"/"${target##*/}" "$SHARUN_BIN_DIR"/"${realbwrap##*/}" - else - # this should never happen, we are gonna exit without error however - # since maybe older versions of webkit2gtk can be installed without bwrap? - _err_msg "Something went very wrong here because bwrap was not deployed" - return 0 - fi - - cat <<-'EOF' > "$cfile" - /* - * anylinux-bwrap-wrapper — intercepts bubblewrap to inject essential bind mounts - * and remap hardcoded paths inside the sandbox. - * - * Many applications sandbox themselves via bwrap (e.g. WebKitGTK) but dont - * account for AppImage paths. Symlinks created by quick-sharun in /tmp become - * unresolvable inside the sandbox. This wrapper injects: - * --bind $APPDIR $APPDIR so the AppImage mount is visible inside - * --bind /tmp /tmp so sharun's /tmp symlinks stay valid - * --setenv SHARUN_DIR ... so child processes know the symlink prefix - * --setenv APPDIR ... so the AppDir path survives into the sandbox - * --setenv PATH ... so binaries in $APPDIR/bin get executed always - * --proc /proc so that sharun can read /proc/self/exe and work - * - * It also rewrites hardcoded command paths (e.g. /usr/bin/xdg-dbus-proxy) to - * their AppDir equivalents when found, so the AppImage's bundled binaries are - * used instead of the host. - * - * Two codepaths: "--args N" (options passed through a pipe) and plain argv. - */ - - #define _GNU_SOURCE - #include - #include - #include - #include - #include - #include - #include - - /* ---- Option tables ------------------------------------------------ */ - /* - * bwrap options and how many trailing arguments they consume. - * Used by find_cmd_idx() to skip over values and not mistake them - * for the command to execute. - */ - static int opt_arg_count(const char *arg) - { - static const struct { const char *name; int n; } table[] = { - { "--overlay", 3 }, /* RWSRC WORKDIR DEST */ - { "--bind", 2 }, { "--ro-bind", 2 }, - { "--bind-try", 2 }, { "--ro-bind-try", 2 }, - { "--dev-bind", 2 }, { "--dev-bind-try", 2 }, - { "--bind-data", 2 }, { "--ro-bind-data", 2 }, - { "--file", 2 }, { "--ro-file", 2 }, - { "--dev-mknod", 2 }, { "--symlink", 2 }, - { "--chmod", 2 }, { "--bind-fd", 2 }, - { "--ro-bind-fd", 2 }, { "--setenv", 2 }, - { "--tmpfs", 1 }, { "--proc", 1 }, - { "--dev", 1 }, { "--devpts", 1 }, - { "--mqueue", 1 }, { "--hostname", 1 }, - { "--seccomp", 1 }, { "--block-fd", 1 }, - { "--userns", 1 }, { "--uid", 1 }, - { "--gid", 1 }, { "--chdir", 1 }, - { "--unsetenv", 1 }, { "--lock-file", 1 }, - { "--sync-fd", 1 }, { "--info-fd", 1 }, - { "--json-status-fd", 1 }, { "--add-seccomp-fd", 1 }, - { "--add-feature", 1 }, { "--args", 1 }, - { "--dir", 1 }, { "--remount-ro", 1 }, - { "--perms", 1 }, { "--size", 1 }, - { "--argv0", 1 }, { "--overlay-src", 1 }, - { "--tmp-overlay", 1 }, { "--ro-overlay", 1 }, - { "--exec-label", 1 }, { "--file-label", 1 }, - { "--userns-block-fd", 1 }, { "--pidns", 1 }, - { NULL, 0 } - }; - for (int i = 0; table[i].name; i++) - if (strcmp(arg, table[i].name) == 0) return table[i].n; - return 0; - } - - /* ---- Helpers ------------------------------------------------------ */ - - /* Split a NUL-separated buffer into a null-terminated string array. */ - static int parse_content(const char *buf, size_t len, char ***out) - { - int cap = 64, n = 0; - char **arr = calloc(cap, sizeof(char *)); - if (!arr) return -1; - - for (size_t i = 0, start = 0; i <= len; i++) { - if (i == len || buf[i] == '\0') { - size_t slen = i - start; - if (slen == 0) { start = i + 1; continue; } - if (n >= cap) { - cap *= 2; - char **tmp = realloc(arr, cap * sizeof(char *)); - if (!tmp) { for (int k = 0; k < n; k++) free(arr[k]); free(arr); return -1; } - arr = tmp; - } - char *s = malloc(slen + 1); - memcpy(s, buf + start, slen); - s[slen] = '\0'; - arr[n++] = s; - start = i + 1; - } - } - *out = arr; - return n; - } - - /* Read all data from a file descriptor until EOF. */ - static char *read_fd(int fd, size_t *out_len) - { - size_t cap = 4096, len = 0; - char *buf = malloc(cap); - if (!buf) return NULL; - ssize_t n; - while ((n = read(fd, buf + len, cap - len)) > 0) { - len += n; - if (len == cap) { - cap *= 2; - char *tmp = realloc(buf, cap); - if (!tmp) { free(buf); return NULL; } - buf = tmp; - } - } - if (n < 0) { free(buf); return NULL; } - buf[len] = '\0'; - *out_len = len; - return buf; - } - - /* Serialize a string array as NUL-separated bytes into a new pipe; return the read fd. */ - static int serialize_to_pipe(char **args, int n) - { - size_t len = 0; - for (int i = 0; i < n; i++) len += strlen(args[i]) + 1; - - char *buf = malloc(len); - if (!buf) return -1; - size_t pos = 0; - for (int i = 0; i < n; i++) { - size_t sl = strlen(args[i]); - memcpy(buf + pos, args[i], sl); - pos += sl; - buf[pos++] = '\0'; - } - - int fds[2]; - if (pipe(fds) != 0) { free(buf); return -1; } - fcntl(fds[1], F_SETFD, FD_CLOEXEC); /* write end only */ - size_t off = 0; - while (off < len) { - ssize_t w = write(fds[1], buf + off, len - off); - if (w <= 0) break; - off += w; - } - close(fds[1]); - free(buf); - return fds[0]; - } - - /* - * Walk bwrap args to find where the command starts. - * Returns the index of "--" or the first non-option item, - * or n if all items are options. - */ - static int find_cmd_idx(char **args, int n) - { - for (int i = 0; i < n; i++) { - if (strcmp(args[i], "--") == 0) return i; - if (args[i][0] == '-') { i += opt_arg_count(args[i]); continue; } - return i; - } - return n; - } - - /* - * Build the array of --bind/--setenv options we inject into bwrap. - * Returns the count of items placed in *out, or -1 on failure. - */ - static int build_injections(const char *appdir, const char *sharun_dir, - const char *path, char ***out) - { - struct { const char *flag, *a, *b; } entries[] = { - { "--proc", "/proc", NULL }, /* sharun needs /proc for /proc/self/exe to resolve symlinks */ - { "--bind", appdir, appdir }, /* AppDir visible inside */ - { "--bind", "/tmp", "/tmp" }, /* must follow webkit's --tmpfs /tmp */ - { "--setenv", "SHARUN_DIR", sharun_dir }, - { "--setenv", "APPDIR", appdir }, - { "--setenv", "PATH", path }, - }; - int nentries = (int)(sizeof(entries) / sizeof(entries[0])); - - /* Count how many slots we need (skip entries with NULL values for 'a') */ - int cap = 0; - for (int i = 0; i < nentries; i++) { - if (!entries[i].a) continue; - cap += entries[i].b ? 3 : 2; - } - - char **arr = calloc(cap + 1, sizeof(char *)); - if (!arr) return -1; - - int j = 0; - for (int i = 0; i < nentries; i++) { - if (!entries[i].a) continue; - arr[j++] = strdup(entries[i].flag); - arr[j++] = strdup(entries[i].a); - if (entries[i].b) - arr[j++] = strdup(entries[i].b); - } - arr[j] = NULL; - *out = arr; - return j; - } - - /* - * Try execvp of bwrap.wrapped, falling back to the same directory - * as this wrapper (shared/bin/). - */ - static void exec_binary(char **argv) - { - execvp("bwrap.wrapped", argv); - char self[PATH_MAX]; - ssize_t len = readlink("/proc/self/exe", self, sizeof(self) - 1); - if (len > 0 && (size_t)len < sizeof(self) - 14) { - self[len] = '\0'; - char *slash = strrchr(self, '/'); - if (slash) { - *slash = '\0'; - char path[PATH_MAX + 16]; - snprintf(path, sizeof(path), "%s/bwrap.wrapped", self); - execv(path, argv); - } - } - fprintf(stderr, "anylinux-bwrap-wrapper: failed to exec bwrap.wrapped: %s\n", - strerror(errno)); - _exit(1); - } - - /* - * If a path looks like a hardcoded system binary, check whether the AppDir - * ships it. Returns a malloc'd AppDir path, or NULL if no match found. - */ - static char *try_remap_path(const char *path, const char *appdir) - { - if (!appdir || !path || path[0] != '/') return NULL; - const char *base = strrchr(path, '/'); - if (!base || !base[1]) return NULL; - base++; - - char buf[PATH_MAX]; - const char *dirs[] = { "bin", "lib", "libexec", NULL }; - for (int i = 0; dirs[i]; i++) { - snprintf(buf, sizeof(buf), "%s/%s/%s", appdir, dirs[i], base); - if (access(buf, X_OK) == 0) return strdup(buf); - } - return NULL; - } - - /* - * Scan exec argv for the command and replace it with an AppDir equivalent - * when one exists (e.g. /usr/bin/xdg-dbus-proxy → $APPDIR/bin/xdg-dbus-proxy). - */ - static void remap_argv_command(char **new_argv, const char *appdir) - { - if (!appdir) return; - for (int i = 1; new_argv[i]; i++) { - if (strcmp(new_argv[i], "--") == 0) { - if (new_argv[i + 1]) { - char *r = try_remap_path(new_argv[i + 1], appdir); - if (r) { free(new_argv[i + 1]); new_argv[i + 1] = r; } - } - return; - } - if (new_argv[i][0] == '-') { i += opt_arg_count(new_argv[i]); continue; } - char *r = try_remap_path(new_argv[i], appdir); - if (r) { free(new_argv[i]); new_argv[i] = r; } - return; - } - } - - /* ---- Main --------------------------------------------------------- */ - - int main(int argc, char *argv[]) - { - const char *appdir = getenv("APPDIR"); - const char *sharun_dir = getenv("SHARUN_DIR"); - const char *path = getenv("PATH"); - - if (argc < 2) { - fprintf(stderr, "Usage: anylinux-bwrap-wrapper [bwrap options...]\n"); - return 1; - } - - /* Build the options we will inject */ - char **injections; - int inject_count = build_injections(appdir, sharun_dir, path, &injections); - if (inject_count < 0) { - fprintf(stderr, "anylinux-bwrap-wrapper: malloc failed\n"); - return 1; - } - - /* - * Check if this is a "--args N" invocation (webkit passes its - * complex option list through a pipe). If so, the options are - * in the pipe and argv just holds --args N -- . - * We must rewrite the pipe content with our binds appended. - */ - int args_fd = -1, args_idx = -1; - for (int i = 1; i < argc; i++) { - if (strcmp(argv[i], "--args") == 0 && i + 1 < argc) { - args_fd = atoi(argv[i + 1]); - args_idx = i; - break; - } - } - - if (args_fd >= 0) { - /* ---- --args N path ---- */ - - /* Read the NUL-separated options from the fd */ - size_t content_len; - char *content = read_fd(args_fd, &content_len); - if (!content) { - fprintf(stderr, "anylinux-bwrap-wrapper: failed to read --args fd %d\n", args_fd); - return 1; - } - close(args_fd); - - char **fd_args; - int n = parse_content(content, content_len, &fd_args); - free(content); - if (n < 0) { - fprintf(stderr, "anylinux-bwrap-wrapper: parse_content failed\n"); - return 1; - } - - /* - * Find where the command begins. Items before cmd_idx are - * bwrap options and go (with injections appended) to a new - * pipe. Items at/after cmd_idx (-- / command / args) go to - * the exec argv instead. - */ - int cmd_idx = find_cmd_idx(fd_args, n); - int opts_seccomp = 0; - for (int i = 0; i < cmd_idx; i++) { - if (strcmp(fd_args[i], "--seccomp") == 0) { - opts_seccomp++; - i++; - } - } - - /* Build options array (original options minus --seccomp plus injections) */ - int opt_n = cmd_idx - 2 * opts_seccomp; - int total_opt_n = opt_n + inject_count; - char **opt_args = calloc(total_opt_n + 1, sizeof(char *)); - if (!opt_args) return 1; - int oi = 0; - for (int i = 0; i < cmd_idx; i++) { - if (strcmp(fd_args[i], "--seccomp") == 0) { - i++; /* skip fd number */ - continue; - } - opt_args[oi++] = fd_args[i]; - } - for (int i = 0; i < inject_count; i++) - opt_args[oi++] = injections[i]; - opt_args[total_opt_n] = NULL; - - int new_fd = serialize_to_pipe(opt_args, total_opt_n); - free(opt_args); - if (new_fd < 0) { - fprintf(stderr, "anylinux-bwrap-wrapper: pipe failed\n"); - return 1; - } - - /* - * Build exec argv: - * bwrap.wrapped --args NEWFD [original argv minus --args N] [fd command] - */ - int new_argc = argc + (n - cmd_idx); - char **new_argv = calloc(new_argc + 1, sizeof(char *)); - if (!new_argv) return 1; - - char fd_str[32]; - snprintf(fd_str, sizeof(fd_str), "%d", new_fd); - - new_argv[0] = "bwrap.wrapped"; - int j = 1; - for (int i = 1; i < argc; i++) { - if (i == args_idx) { - new_argv[j++] = strdup("--args"); - new_argv[j++] = strdup(fd_str); - i++; /* skip the old fd number */ - } else { - new_argv[j++] = strdup(argv[i]); - } - } - for (int i = cmd_idx; i < n; i++) - new_argv[j++] = fd_args[i]; - new_argv[j] = NULL; - - remap_argv_command(new_argv, appdir); - exec_binary(new_argv); - /* not reached */ - } - - /* ---- Direct argv path (no --args) ---- */ - - /* Build argv without --seccomp N (this blocks lstat and breaks sharun) */ - int st_argc = 0; - char **st_argv = calloc(argc + 1, sizeof(char *)); - if (!st_argv) return 1; - st_argv[st_argc++] = argv[0]; - for (int i = 1; i < argc; i++) { - if (strcmp(argv[i], "--seccomp") == 0) { - i++; /* skip fd number */ - continue; - } - st_argv[st_argc++] = argv[i]; - } - st_argv[st_argc] = NULL; - - /* - * Find where to insert our binds in the stripped argv: - * before "--" or before the first non-option argument (the command). - */ - int insert_at = 1 + find_cmd_idx(st_argv + 1, st_argc - 1); - - /* Build exec argv with injections inserted at insert_at */ - int new_argc = st_argc + inject_count; - char **new_argv = calloc(new_argc + 1, sizeof(char *)); - if (!new_argv) return 1; - - new_argv[0] = "bwrap.wrapped"; - int j = 1; - for (int i = 1; i < st_argc; i++) { - if (i == insert_at) { - for (int k = 0; k < inject_count; k++) - new_argv[j++] = injections[k]; - } - new_argv[j++] = strdup(st_argv[i]); - } - /* If no suitable insertion point was found, tack on at the end */ - if (insert_at == st_argc) { - for (int k = 0; k < inject_count; k++) - new_argv[j++] = injections[k]; - } - new_argv[j] = NULL; - - free(st_argv); - - remap_argv_command(new_argv, appdir); - exec_binary(new_argv); - return 1; - } - EOF - - cc -Wall -Wextra -O2 -o "$SHARUN_BIN_DIR"/"${target##*/}" "$cfile" - ln "$APPDIR"/sharun "$target" - chmod +x "$target" - _echo "* added bwrap wrapper!" -} - -_fix_cpython_ldconfig_mess() { - # cpython runs ldconfig -p to determine library names, this is - # super flawed because ldconfig -p is going to print host libraries - # and not our bundled libraries, it also only works in glibc systems - # - # it also hardcodes /sbin/ldconfig and resets PATH variable - # so we have to do a lot of patches here to fix this mess - # - # we will patch /sbin/ldconfig for _ldconfig to avoid conflicts, see: - # https://github.com/pkgforge-dev/ghostty-appimage/issues/122 - - set -- "$DST_LIB_DIR"/python*/ctypes/util.py - ldconfig=$DST_BIN_DIR/_ldconfig - if [ -x "$ldconfig" ]; then - return 0 - elif [ ! -f "$1" ]; then - return 0 # exit without error if ctypes is not present - fi - pythonlib=$1 - - # patch ctypes lib - sed -i \ - -e 's|/sbin/ldconfig|_ldconfig|g' \ - -e 's|env={.*}||' \ - "$pythonlib" - - cat <<-'EOF' > "$ldconfig" - #!/bin/sh - - # wrapper that makes ldconfig -p print our bundled libraries - export LC_ALL=C - export LANG=C - # some distros don't include /sbin in PATH - export PATH="$PATH:/usr/sbin:/sbin" - - if [ -z "$APPDIR" ]; then - APPDIR=$(cd "${0%/*}"/../ && echo "$PWD") - fi - - _list_libs() { - echo "69420 libs found in cache \`/etc/ld.so.cache'" - - case "$(uname -m)" in - aarch64) arch=AArch64;; - *) arch=x86-64;; - esac - - for f in "$APPDIR"/lib*/*.so* "$APPDIR"/lib*/*/*.so*; do - echo " ${f##*/} (libc6,$arch) => $f" - done - - echo "Cache generated by: ldconfig (GNU libc) stable release version 2.42" - } - - # lets try to use the real thing - case "$1" in - -p|--print-cache) - _list_libs - ;; - *) - exec ldconfig "$@" - ;; - esac - EOF - chmod +x "$ldconfig" - - _echo "* patched cpython /sbin/ldconfig for _ldconfig wrapper" - - # This python library ships a certificate with no way to override! - # https://github.com/certifi/python-certifi/issues/271 - # https://github.com/certifi/python-certifi/issues/200 - # - # some distros replace it with a symlink to the host certs, we have to - # make sure to ship the actual certificate since there is no override... - # - set -- "$DST_LIB_DIR"/python*/site-packages/certifi/cacert.pem - if [ -L "$1" ] && c=$(readlink -f "$1"); then - rm -f "$1" - cp "$c" "$1" - fi - - # pysdl is even more broken - set -- "$DST_LIB_DIR"/python*/site-packages/sdl3/__init__.py - [ -f "$1" ] || return 0 - sed -i \ - -e 's|if os.path.exists(path) and SDL|if SDL|' \ - -e 's|binaryMap\[module\] =.*|binaryMap[module] = ctypes.CDLL(path)|' \ - "$1" - _echo "* fixed pysdl broken mess... this may not work always!" -} - -_add_path_mapping_hardcoded() { - if [ -f "$PATH_MAPPING_SCRIPT" ]; then - return 0 - fi - cat <<-'EOF' > "$PATH_MAPPING_SCRIPT" - #!/bin/sh - - # this script makes symnlinks to hardcoded random dirs that - # were patched away by quick-sharun when hardcoded paths are - # detected or when 'PATH_MAPPING_HARDCODED' is used - - _tmp_bin="" - _tmp_lib="" - _tmp_share="" - - if [ -n "$_tmp_bin" ]; then - LC_ALL=C ln -sfn "$APPDIR"/bin /tmp/"$_tmp_bin" || : - fi - if [ -n "$_tmp_lib" ]; then - LC_ALL=C ln -sfn "$APPDIR"/lib /tmp/"$_tmp_lib" || : - fi - if [ -n "$_tmp_share" ]; then - LC_ALL=C ln -sfn "$APPDIR"/share /tmp/"$_tmp_share" || : - fi - EOF - _echo "* Added $PATH_MAPPING_SCRIPT" -} - -_patch_away_usr_bin_dir() { - set -- "$(readlink -f "$1")" - if ! grep -Eaoq -m 1 "/usr/bin" "$1"; then - return 1 - fi - - sed -i -e "s|/usr/bin|/tmp/$_tmp_bin|g" "$1" - - _echo "* patched away /usr/bin from $1" - _add_path_mapping_hardcoded || exit 1 - - sed -i -e "s|_tmp_bin=.*|_tmp_bin=$_tmp_bin|g" "$PATH_MAPPING_SCRIPT" -} - -_patch_away_usr_lib_dir() { - set -- "$(readlink -f "$1")" - if ! grep -Eaoq -m 1 "/usr/lib" "$1"; then - return 1 - fi - - sed -i -e "s|/usr/lib|/tmp/$_tmp_lib|g" "$1" - - _echo "* patched away /usr/lib from $1" - _add_path_mapping_hardcoded || exit 1 - - sed -i -e "s|_tmp_lib=.*|_tmp_lib=$_tmp_lib|g" "$PATH_MAPPING_SCRIPT" -} - -_patch_away_usr_share_dir() { - set -- "$(readlink -f "$1")" - if ! grep -Eaoq -m 1 "/usr/share" "$1"; then - return 1 - fi - - sed -i -e "s|/usr/share|/tmp/$_tmp_share|g" "$1" - - _echo "* patched away /usr/share from $1" - _add_path_mapping_hardcoded || exit 1 - - sed -i -e "s|_tmp_share=.*|_tmp_share=$_tmp_share|g" "$PATH_MAPPING_SCRIPT" -} - -_check_hardcoded_lib_dirs() { - # check for hardcoded path to any other possibly bundled library dir - set -- "$DST_LIB_DIR"/* - for d do - [ -d "$d" ] || continue - d=${d##*/} - # skip directories we already handle here or in sharun - case "$d" in - alsa-lib |\ - dri |\ - gbm |\ - gconv |\ - gdk-pixbuf* |\ - gio |\ - gtk* |\ - gstreamer* |\ - gvfs |\ - ImageMagick*|\ - imlib2 |\ - libproxy |\ - locale |\ - pipewire* |\ - pulseaudio |\ - qt* |\ - spa* |\ - vdpau ) - continue - ;; - esac - - for f in "$DST_LIB_DIR"/*.so* "$SHARUN_BIN_DIR"/*; do - if [ ! -f "$f" ]; then - continue - elif grep -aoq -m 1 "$LIB_DIR"/"$d" "$f"; then - _echo "* Detected hardcoded path to $LIB_DIR/$d in $f" - _patch_away_usr_lib_dir "$f" || : - fi - done - done -} - -_check_hardcoded_data_dirs() { - # first check for hardcoded path to /usr/share/fonts and copy if so - src_fonts=/usr/share/fonts - dst_fonts="$APPDIR"/share/fonts - if grep -aoq -m 1 "$src_fonts" "$SHARUN_BIN_DIR"/*; then - if [ -d "$src_fonts" ] && [ ! -d "$dst_fonts" ]; then - mkdir -p "$dst_fonts" - for d in "$src_fonts"/*; do - if [ "${d##*/}" = "Adwaita" ]; then - continue - fi - if [ -e "$d" ]; then - cp -vr "$d" "$dst_fonts" - fi - done - fi - fi - - # now check if any of the bundled datadirs need to be patched - set -- "$APPDIR"/share/* - for d do - [ -d "$d" ] || continue - d=${d##*/} - # skip directories we already handle here or in sharun - case "$d" in - alsa |\ - drirc.d |\ - file |\ - glib-* |\ - glvnd |\ - icons |\ - libdrm |\ - libthai |\ - locale |\ - pipewire*|\ - terminfo |\ - vulkan |\ - X11 ) - continue - ;; - esac - - for f in "$DST_LIB_DIR"/*.so* "$SHARUN_BIN_DIR"/*; do - if [ ! -f "$f" ]; then - continue - elif grep -aoq -m 1 /usr/share/"$d" "$f"; then - _echo "* Detected hardcoded path to /usr/share/$d in $f" - _patch_away_usr_share_dir "$f" || : - fi - done - done -} - -_sort_env_file() { - # make sure the .env has all the "unset" last, due to a bug in the dotenv - # library used by sharun all the unsets have to be declared last in the .env - # also deduplicate since the same var may be set multiple times - if [ -f "$APPDIR"/.env ]; then - sorted_env="$(LC_ALL=C awk ' - { - if ($0 ~ /^unset/) { - unset_array[++u] = $0 - } else { - if (!seen[$0]++) print - } - } - END { - for (i = 1; i <= u; i++) { - print unset_array[i] - } - }' "$APPDIR"/.env - )" - echo "$sorted_env" > "$APPDIR"/.env - fi -} - -_strip_bins_and_libs() { - if [ "$NO_STRIP" = 1 ]; then - return 0 - elif ! _is_cmd strip; then - _err_msg "Skipping strip since 'strip' is NOT installed!" - sleep 5 - return 0 - fi - - if [ "$NO_STRIP" != 'libraries' ]; then - find "$APPDIR" -type f -name '*.so*' \ - -exec strip -s -R .comment --strip-unneeded {} \; || : - _echo "* stripped libraries" - fi - - if [ "$NO_STRIP" != 'binaries' ]; then - while IFS="" read -r f; do - if [ ! -x "$f" ]; then - continue - elif _is_bun_binary "$f"; then - continue # bun binaries are delicate - elif _is_pyinstaller_binary "$f"; then - continue # same story as bun binaries - fi - case "$f" in - */python*) continue;; # python interpreter also breaks - esac - strip -s -R .comment --strip-unneeded "$f" || : - done <<-EOF - $(find "$SHARUN_BIN_DIR"/ "$APPDIR"/lib*/ -type f) - EOF - _echo "* stripped binaries" - fi -} - -_add_apprun() { - # sharun needs to be the AppRun while our AppRun is named AppRun.sh, sharun will - # then execute AppRun.sh with whatever shell it can find on the system or AppDir - # this allows AppImages to work on systems without /bin/sh or /usr/bin/env - ln -f "$APPDIR"/sharun "$APPDIR"/AppRun - - f=$APPDIR/AppRun.sh - if [ -f "$f" ]; then - return 0 - fi - _echo "Adding '$f'..." - cat <<-'EOF' > "$f" - #!/bin/sh - - # Example AppRun for using the hooks of this repository. - # NOTE: It is meant to be used with sharun which uses a top level bin dir - - if [ "$APPRUN_DEBUG" = 1 ]; then - set -x - fi - - set -e - - APPDIR=$(cd "${0%/*}" && echo "$PWD") - MAIN_BIN=@MAIN_BIN@ - ARG0="${ARGV0:-$0}" - - unset ARGV0 - - export APPIMAGE_ARCH=@APPIMAGE_ARCH@ - export HOSTPATH=$PATH - export PATH=$APPDIR/bin:$PATH - export ARG0 APPDIR PATH - - # Allow users to set env variables for specific AppImage - # This feature only works with the uruntime - if [ "$1" = '--appimage-add-env' ]; then - shift - for v do - echo "$v" >> "$APPIMAGE".env - >&2 echo "Added '$v' to $APPIMAGE.env" - done - exit 0 - fi - - if [ -f "$APPDIR"/AppRun.lib ]; then - . "$APPDIR"/AppRun.lib - for hook in $APPDIR/bin/*.hook; do - [ -e "$hook" ] || continue - . "$hook" - done - fi - - # Check if ARG0 matches a binary, fallback to $1, then binary in .desktop - if [ -f "$APPDIR"/bin/"${ARG0##*/}" ]; then - TO_LAUNCH=$APPDIR/bin/${ARG0##*/} - elif [ -f "$APPDIR"/bin/"$1" ]; then - TO_LAUNCH=$APPDIR/bin/$1 - shift - else - TO_LAUNCH=$APPDIR/bin/$MAIN_BIN - fi - - set -- "$TO_LAUNCH" "$@" - - # If LD_DEBUG=libs is set outside the AppImage the output is not helpful - # because it will include the libs of sh, grep, cat, etc from the hooks - # with this var we can set LD_DEBUG=libs for the bundled application only - if [ "$APPIMAGE_DEBUG" = 1 ]; then - cat /etc/os-release >"$PWD"/"${APPIMAGE##*/}"-debug.log || : - export LD_DEBUG=libs - export VK_LOADER_DEBUG=all - export LIBGL_DEBUG=verbose - export EGL_LOG_LEVEL=debug - export LC_ALL=C - export SHARUN_PRINTENV=1 - "$@" 2>>"$PWD"/"${APPIMAGE##*/}"-debug.log || : - >&2 echo "Debug log at: '$PWD/${APPIMAGE##*/}-debug.log'" - else - exec "$@" - fi - EOF - - chmod +x "$f" - - sed -i \ - -e "s|@MAIN_BIN@|$MAIN_BIN|" \ - -e "s|@APPIMAGE_ARCH@|$APPIMAGE_ARCH|" \ - "$f" - - _echo "* Added $f" -} - -_add_hooks_library() { - f=$APPDIR/AppRun.lib - if [ -f "$f" ]; then - return 0 - fi - _echo "Adding '$f'..." - cat <<-'EOF' > "$f" - #!/bin/sh - - HOST_HOME=${REAL_HOME:-$HOME} - HOST_XDG_CONFIG_HOME=${REAL_XDG_CONFIG_HOME:-${XDG_CONFIG_HOME:-$HOST_HOME/.config}} - HOST_XDG_DATA_HOME=${REAL_XDG_DATA_HOME:-${XDG_DATA_HOME:-$HOST_HOME/.local/share}} - HOST_XDG_CACHE_HOME=${REAL_XDG_CACHE_HOME:-${XDG_CACHE_HOME:-$HOST_HOME/.cache}} - HOST_XDG_STATE_HOME=${REAL_XDG_STATE_HOME:-${XDG_STATE_HOME:-$HOST_HOME/.local/state}} - - export HOST_HOME HOST_XDG_CONFIG_HOME HOST_XDG_DATA_HOME HOST_XDG_CACHE_HOME HOST_XDG_STATE_HOME - - BINDIR=${XDG_BIN_HOME:-~/.local/bin} - DATADIR=${XDG_DATA_HOME:-~/.local/share} - CONFIGDIR=${XDG_CONFIG_HOME:-~/.config} - CACHEDIR=${XDG_CACHE_HOME:-~/.cache} - STATEDIR=${XDG_STATE_HOME:-~/.local/state} - - # always change XDG_CACHE_HOME to our own dedicated location - # using the host XDG_CACHE_HOME has been a source of issues - # See: https://github.com/pkgforge-dev/Anylinux-AppImages/issues/657 - if [ "$USE_HOST_XDG_CACHE_HOME" != 1 ] && [ -n "$APPIMAGE" ]; then - case "$XDG_CACHE_HOME" in - *"$APPIMAGE"*) # make sure we are not using the portable cache first - : - ;; - *) - _cache_dir=$CACHEDIR/AppImage-Cache - if [ -d "$_cache_dir" ] || mkdir -p "$_cache_dir" 2>/dev/null; then - export XDG_CACHE_HOME="$_cache_dir" - fi - ;; - esac - fi - - err_msg(){ - >&2 printf '\033[1;31m%s\033[0m\n' " $*" - } - - is_cmd() { - if [ "$1" = '--any' ]; then - shift - for cmd do - if command -v "$cmd" 1>/dev/null; then - return 0 - fi - done - return 1 - else - for cmd do - command -v "$cmd" 1>/dev/null || return 1 - done - fi - return 0 - } - - run_gui_sudo() { - if [ "$(id -u)" = 0 ]; then _sudocmd="" - elif _sudocmd=$(command -v pkexec); then : - elif _sudocmd=$(command -v lxqt-sudo); then : - elif _sudocmd=$(command -v run0); then set -- --via-shell "$@" - fi - if [ "$1" = --check ]; then - [ -n "$_sudocmd" ] || [ "$(id -u)" = 0 ] || return 1 - return 0 - else - if [ -z "$_sudocmd" ] && [ "$(id -u)" != 0 ]; then - err_msg "We need 'pkexec' or 'lxqt-sudo' or 'run0' to perform this operation" - return 1 - fi - fi - $_sudocmd "$@" - } - - download() { - if _download_cmd=$(command -v wget); then set -- -O "$@" - elif _download_cmd=$(command -v curl); then set -- -Lo "$@" - else - err_msg "We need 'wget' or 'curl' to download $1" - return 1 - fi - log=${TMPDIR:-/tmp}/._download.log - if ! "$_download_cmd" "$@" 2>"$log"; then - cat "$log" - err_msg "Download failed!" - return 1 - fi - rm -f "$log" - } - - - # the following function are used by notify - - # display functions, these might return non 0 depending on user input - _display_info() { - set -- "INFO: $*" - if is_cmd kdialog; then kdialog --msgbox "$*" - elif is_cmd qarma; then qarma --info --text "$*" - elif is_cmd yad; then yad --info --text "$*" - elif is_cmd zenity; then zenity --info --text "$*" - elif is_cmd gxmessage; then gxmessage -center "$*" - elif is_cmd xmessage; then xmessage -center "$*" - else _notification=0 _display_with_host_term "$*" - fi - } - - _display_error() { - set -- "ERROR: $*" - if is_cmd kdialog; then kdialog --error "$*" - elif is_cmd qarma; then qarma --error --text "$*" - elif is_cmd yad; then yad --error --text "$*" - elif is_cmd zenity; then zenity --error --text "$*" - elif is_cmd gxmessage; then gxmessage -center "$*" - elif is_cmd xmessage; then xmessage -center "$*" - else _notification=0 _display_with_host_term "$*" - fi - } - - _display_warning() { - set -- "WARNING: $*" - if is_cmd kdialog; then kdialog --sorry "$*" - elif is_cmd qarma; then qarma --warning --text "$*" - elif is_cmd yad; then yad --warning --text "$*" - elif is_cmd zenity; then zenity --warning --text "$*" - elif is_cmd gxmessage; then gxmessage -center "$*" - elif is_cmd xmessage; then xmessage -center "$*" - else _notification=0 _display_with_host_term "$*" - fi - } - - _display_question() { - set -- "QUESTION: $*" - if is_cmd kdialog; then kdialog --yesno "$*" - elif is_cmd qarma; then qarma --question --text "$*" - elif is_cmd yad; then yad --question --text "$*" - elif is_cmd zenity; then zenity --question --text "$*" - elif is_cmd gxmessage; then gxmessage -center -buttons "Yes:0,No:1" "$*" - elif is_cmd xmessage; then xmessage -center -buttons "Yes:0,No:1" "$*" - else _notification=0 _display_with_host_term "$*" - fi - } - - # notify functions, these will always return 0 unless there are no deps - _notify_info() { - set -- "INFO: $*" - if is_cmd notify-send; then notify-send "$*" || : - elif is_cmd qarma; then qarma --info --text "$*" || : - elif is_cmd kdialog; then kdialog --passivepopup "$*" || : - elif is_cmd yad; then yad --window-type=notification --text "$*" || : - elif is_cmd zenity; then zenity --info --text "$*" || : - elif is_cmd xmessage; then xmessage -center "$*" || : - elif is_cmd gxmessage; then gxmessage -center "$*" || : - else _notification=1 _display_with_host_term "$*" - fi - } - - _notify_error() { - set -- "ERROR: $*" - if is_cmd notify-send; then notify-send -u critical "$*" || : - elif is_cmd kdialog; then kdialog --error "$*" || : - elif is_cmd qarma; then qarma --error --text "$*" || : - elif is_cmd yad; then yad --window-type=notification --text "$*" || : - elif is_cmd zenity; then zenity --error --text "$*" || : - elif is_cmd xmessage; then xmessage -center "$*" || : - elif is_cmd gxmessage; then gxmessage -center "$*" || : - else _notification=1 _display_with_host_term "$*" - fi - } - - _notify_warning() { - set -- "WARNING: $*" - if is_cmd notify-send; then notify-send -u critical "$*" || : - elif is_cmd kdialog; then kdialog --sorry "$*" || : - elif is_cmd qarma; then qarma --warning --text "$*" || : - elif is_cmd yad; then yad --window-type=notification --text "$*" || : - elif is_cmd zenity; then zenity --warning --text "$*" || : - elif is_cmd gxmessage; then gxmessage -center "$*" || : - elif is_cmd xmessage; then xmessage -center "$*" || : - else _notification=1 _display_with_host_term "$*" - fi - } - - # extreme measure - _display_with_host_term() { - _message=$* - _tmpfile=${TMPDIR:-/tmp}/.${0##*/}-no-gui-fallback - - cmd_notification="echo '$_message'; read yn" - cmd_display=" - trap 'echo 0 > \"$_tmpfile\"; exit' HUP TERM - echo '$_message' - printf '\n%s'' (Yes/No)?: '; - while :; do - read yn - case \$yn in - Y*|y*) echo 1 > '$_tmpfile'; break;; - N*|n*) echo 0 > '$_tmpfile'; break;; - *) echo 'Please type Yes or No' ;; - esac - done - " - - if [ "$_notification" = 1 ]; then - tcmd="$cmd_notification" - else - tcmd="$cmd_display" - fi - - # normal terminals - if is_cmd alacritty; then alacritty -e sh -c "$tcmd" & - elif is_cmd wezterm; then wezterm -e sh -c "$tcmd" & - elif is_cmd konsole; then konsole -e sh -c "$tcmd" & - elif is_cmd lxterminal; then lxterminal -e sh -c "$tcmd" & - elif is_cmd kitty; then kitty -e sh -c "$tcmd" & - elif is_cmd urxvt; then urxvt -e sh -c "$tcmd" & - elif is_cmd xterm; then xterm -e sh -c "$tcmd" & - # mmmm - elif is_cmd gnome-terminal; then gnome-terminal -- sh -c "$tcmd" & - # these need extra quotes for some reason - elif is_cmd ptyxis; then ptyxis -x "sh -c \"$tcmd\"" & - elif is_cmd qterminal; then qterminal -e "sh -c \"$tcmd\"" & - elif is_cmd mate-terminal; then mate-terminal -e "sh -c \"$tcmd\"" & - elif is_cmd xfce4-terminal; then xfce4-terminal -e "sh -c \"$tcmd\"" & - else - err_msg "Cannot find suitable binary to perform operation!" - return 127 - fi - - if [ "$_notification" = 1 ]; then - return 0 - fi - - _elapsed=0 - _timeout=150 # 15 seconds - while :; do - if [ -f "$_tmpfile" ] || [ "$_elapsed" -ge "$_timeout" ]; then - break - fi - sleep 0.1 - _elapsed=$(( _elapsed + 1 )) - done - - read -r _reply < "$_tmpfile" - rm -f "$_tmpfile" - - if [ "$_reply" = "1" ]; then - return 0 - else - return 1 - fi - } - - notify() { - case "$1" in - --display-info|-di) shift; _display_info "$@";; - --display-error|-de) shift; _display_error "$@";; - --display-warning|-dw) shift; _display_warning "$@";; - --display-question|-dq) shift; _display_question "$@";; - --notify-info|-ni) shift; _notify_info "$@";; - --notify-error|-ne) shift; _notify_error "$@";; - --notify-warning|-nw) shift; _notify_warning "$@";; - # act as notify-send ARG wrapper when no flag is given - *) _notify_info "$@";; - esac - } - EOF - chmod +x "$f" - _echo "* Added $f" -} - -_handle_nested_bins() { - # wrap any executable in lib with sharun - for b in $(find "$DST_LIB_DIR"/ -type f ! -name '*.so*'); do - if [ -x "$b" ] && [ -x "$SHARUN_BIN_DIR"/"${b##*/}" ]; then - rm -f "$b" - ln "$APPDIR"/sharun "$b" - _echo "* Wrapped lib executable '$b' with sharun" - fi - done - - # do the same for possible nested binaries in bin - for b in $(find "$DST_BIN_DIR"/*/ -type f ! -name '*.so*' 2>/dev/null); do - if [ -x "$b" ] && [ -x "$SHARUN_BIN_DIR"/"${b##*/}" ]; then - rm -f "$b" - ln "$APPDIR"/sharun "$b" - _echo "* Wrapped nested bin executable '$b' with sharun" - fi - done -} - -# sometimes developers add stuff like /bin/sh or env as the Exec= key of the -# desktop entry, 99.99% of the time this is not wanted, so we have to error that -_check_main_bin() { - if [ -z "$MAIN_BIN" ]; then - MAIN_BIN=$(awk -F'=| ' '/^Exec=/{print $2; exit}' "$DESKTOP_ENTRY" | tr -d "\"'") - MAIN_BIN=${MAIN_BIN##*/} - case "$MAIN_BIN" in - env|sh|bash) - _err_msg "Main binary is '$MAIN_BIN', it is unlikely you" - _err_msg "are actually going to package '$MAIN_BIN'" - _err_msg "as an appimage, bailing out..." - _err_msg "set MAIN_BIN=$MAIN_BIN if you want to do this." - exit 1 - ;; - esac - fi - - if [ -f "$DST_BIN_DIR"/"$MAIN_BIN" ]; then - return 0 - fi - - _err_msg "Main binary is set to '$MAIN_BIN', but this file is NOT present" - _err_msg "This is the default binary to be launched in this application" - _err_msg "Please make sure to bundle $MAIN_BIN" - _err_msg "By default the main binary is taken from the top level desktop" - _err_msg "entry in '$APPDIR', make sure to add the correct desktop entry" - exit 1 -} - -_make_static_bin() ( - ONELF=$TMPDIR/onelf - if [ ! -x "$ONELF" ]; then - _echo "Downloading onelf..." - _download "$ONELF" "$ONELF_LINK" - chmod +x "$ONELF" - fi - - mkdir -p "$DST_BIN_DIR" - _echo "------------------------------------------------------------" - for bin do - b=${bin##*/} - _echo "Packing $bin as a static binary with onelf..." - - _tmpdir=$TMPDIR/.onelf_build_$$_$b - rm -rf "$_tmpdir" - mkdir -p "$_tmpdir" - - "$ONELF" bundle-libs "$_tmpdir" --from-binary "$bin" --strip --scan-dlopen - "$ONELF" pack "$_tmpdir" -o "$DST_BIN_DIR"/"$b" --command bin/"$b" - rm -rf "$_tmpdir" - done - _echo "------------------------------------------------------------" -) - -_make_appimage() { - _echo "------------------------------------------------------------" - _echo "Making AppImage..." - _echo "------------------------------------------------------------" - - if [ ! -d "$APPDIR" ]; then - _err_msg "ERROR: No $APPDIR directory found" - _err_msg "Set APPDIR if you have it at another location" - exit 1 - elif [ ! -f "$APPDIR"/AppRun ]; then - _err_msg "ERROR: No $APPDIR/AppRun file found!" - exit 1 - fi - chmod +x "$APPDIR"/AppRun - _get_desktop - _get_icon - _sort_env_file - - _echo "------------------------------------------------------------" - if [ -z "$UPINFO" ]; then - echo "No update information given, trying to guess it..." - if [ -n "$GITHUB_REPOSITORY" ]; then - UPINFO="gh-releases-zsync|${GITHUB_REPOSITORY%/*}|${GITHUB_REPOSITORY#*/}|latest|*$ARCH.AppImage.zsync" - _echo "Guessed $UPINFO as the update information" - _echo "It may be wrong so please set the UPINFO instead" - else - _err_msg "We were not able to guess the update information" - _err_msg "Please add it if you will distribute the AppImage" - fi - fi - _echo "------------------------------------------------------------" - - if [ "$DEVEL_RELEASE" = 1 ]; then - if ! grep -q '^Name=.*Nightly' "$DESKTOP_ENTRY"; then - >&2 echo "Adding Nightly to desktop entry name" - sed -i -e 's/^\(Name=.*\)$/\1 Nightly/' "$DESKTOP_ENTRY" - fi - # also change UPINFO to use nightly tag - if [ -n "$UPINFO" ]; then - UPINFO=$(echo "$UPINFO" | sed 's/|latest|/|nightly|/') - fi - fi - - if ! mkdir -p "$OUTPATH"; then - _err_msg "ERROR: Cannot create output directory: '$OUTPATH'" - exit 1 - fi - - if [ ! -x "$APPIMAGETOOL" ]; then - _echo "Downloading appimagetool from $APPIMAGETOOL_LINK" - _download "$APPIMAGETOOL" "$APPIMAGETOOL_LINK" - chmod +x "$APPIMAGETOOL" - fi - - _echo "------------------------------------------------------------" - _echo "Making AppImage..." - _echo "------------------------------------------------------------" - - if ! "$APPIMAGETOOL"; then - _err_msg "ERROR: Something went wrong making the AppImage!" - exit 1 - fi - - set -- "$OUTPATH"/*.AppImage - if [ ! -f "$1" ]; then - _err_msg "ERROR: No AppImage was produced??" - exit 1 - else - chmod +x "$1" - fi - - _echo "------------------------------------------------------------" - _echo "All done! AppImage at: $1" - _echo "------------------------------------------------------------" - exit 0 -} - -case "$1" in - --help) - _help_msg - ;; - --make-appimage) - _make_appimage - ;; - --test) - shift - _test_appimage "$@" - ;; - --simple-test) - shift - _simple_test_appimage "$@" - ;; - --make-static-bin) - shift - _make_static_bin "$@" - exit 0 - ;; - '') - _help_msg - ;; -esac - -_sanity_check -_get_desktop -_get_icon - -_echo "------------------------------------------------------------" -_echo "Starting deployment, checking if extra libraries need to be added..." -echo "" - -_determine_what_to_deploy "$@" -_make_deployment_array - -echo "" -_echo "Now jumping to deploying..." -_echo "------------------------------------------------------------" - -_get_sharun -_deploy_libs "$@" -_check_always_software -_handle_bins_scripts - -if [ "$DEPLOY_FLUTTER" = 1 ]; then - if [ -z "$FLUTTER_LIB" ]; then - _err_msg "Flutter deployment was forced but looks like the" - _err_msg "the application does not link to libflutter at all" - _err_msg "If you see this message please open a bug report!" - exit 1 - fi - - # flutter apps need to have a relative lib and data directory - # we need to find the directory that contains libapp.so - if libapp=$(cd "$DST_BIN_DIR" \ - && find ../lib/ -type f -name 'libapp.so' -print -quit); then - d=${libapp%/*} - if [ ! -d "$DST_BIN_DIR"/"${d##*/}" ]; then - ln -s "$d" "$DST_BIN_DIR"/"${d##*/}" - fi - else - _err_msg "Cannot find libapp.so in $APPDIR" - _err_msg "include it for flutter deployment to work" - fi - - dst_flutter_dir=$DST_BIN_DIR/data - if [ ! -d "$dst_flutter_dir" ]; then - if [ -z "$FLUTTER_DATA_DIR" ]; then - d=${FLUTTER_LIB%/*.so*} - # find data dir, we assume it is relative to - # where libflutter*.so came from - if [ -d "$d"/../data ]; then - FLUTTER_DATA_DIR="$d"/../data - elif [ -d "$d"/../../data ]; then - FLUTTER_DATA_DIR="$d"/../../data - else - _err_msg "Cannot find data directory of $FLUTTER_LIB" - _err_msg "Please set FLUTTER_DATA_DIR to its location" - exit 1 - fi - fi - cp -rv "$FLUTTER_DATA_DIR" "$dst_flutter_dir" - _echo "* Copied flutter data directory" - fi -fi - -echo "" -_echo "------------------------------------------------------------" -echo "" - -_check_main_bin -_map_paths_ld_preload_open -_map_paths_binary_patch -_add_anylinux_lib -_check_window_class -_add_gtk_class_fix - -echo "" -_echo "------------------------------------------------------------" -_echo "Finished deployment! Starting post deployment hooks..." -_echo "------------------------------------------------------------" -echo "" - -# It is common for libraries to have optional dependencies to pipewire they will -# try to dlopen pipewire and if isn't available they fallback to pulseaudio or alsa -# given that it is possible to deploy an application without pipewire we do not -# want that application then dlopen the pipewire of the host and crash -set -- "$DST_LIB_DIR"/libpipewire-0.3.so* -[ -f "$1" ] || no_pipewire=1 - -set -- \ - "$DST_LIB_DIR"/*.so* \ - "$DST_LIB_DIR"/*/*.so* \ - "$DST_LIB_DIR"/*/*/*.so* \ - "$DST_LIB_DIR"/*/*/*/*.so* \ - "$DST_LIB_DIR"/*/*/*/*/*.so* \ - "$DST_LIB_DIR"/*/*/*/*/*/*.so* - -for lib do - [ -f "$lib" ] || continue - # make sure to remove any full rpath from the libs - if patchelf --print-rpath "$lib" | grep -q '^/'; then - patchelf --remove-rpath "$lib" - _echo "* removed full rpath from $lib" - fi - - # also remove full paths from needed libs, for example - # a library may depend on /usr/lib/libkek.so instead of libkek.so - patchelf --print-needed "$lib" | while IFS="" read -r l; do - case "$l" in - /*) - patchelf --replace-needed "$l" "${l##*/}" "$lib" - _echo "* removed full needed lib path $l from $lib" - ;; - esac - done - - if [ "$no_pipewire" = 1 ] && grep -aq -m 1 'libpipewire-0.3.so' "$lib"; then - sed -i -e 's|libpipewire-0.3.so|no-pipewire-kek.so|g' "$lib" - fi -done - -# now start the post deployment hooks -for lib do case "$lib" in - */gio/modules/*.so*) - src_gio_cache=$LIB_DIR/gio/modules/giomodule.cache - dst_gio_cache=$DST_LIB_DIR/gio/modules/giomodule.cache - if [ -f "$src_gio_cache" ] && [ ! -f "$dst_gio_cache" ]; then - cp -v "$src_gio_cache" "$dst_gio_cache" - _echo "* added $src_gio_cache" - fi - ;; - */libgio-*.so*) - f=$DST_BIN_DIR/gio-launch-desktop - if [ ! -x "$f" ]; then - cat <<-'EOF' > "$f" - #!/bin/sh - export GIO_LAUNCHED_DESKTOP_FILE_PID=$$ - exec "$@" - EOF - chmod +x "$f" - _echo "* added $f wrapper" - fi - ;; - */libglib-*.so*) - _glibver=$(echo "$lib" | awk -F'-' '{print $NF}' | sed "s|\.so.*||") - src_glib_schema_dir=/usr/share/glib-$_glibver/schemas - dst_glib_schema_dir=$APPDIR/share/glib-$_glibver/schemas - if [ -d "$src_glib_schema_dir" ] && [ ! -d "$dst_glib_schema_dir" ]; then - mkdir -p "$dst_glib_schema_dir" - cp -r "$src_glib_schema_dir"/* "$dst_glib_schema_dir" - _echo "* added $src_glib_schema_dir" - fi - - # apps may crash when the host has no mime database - src_mime_dir=/usr/share/mime - dst_mime_dir=$APPDIR/share/mime - if [ -d "$src_mime_dir" ] && [ ! -d "$dst_mime_dir" ]; then - cp -r "$src_mime_dir" "$dst_mime_dir" - rm -rf "$dst_mime_dir"/packages # bloat - _echo "* added $src_mime_dir" - fi - ;; - */gdk-pixbuf-*/*/loaders/*.so*) - src_gdkpixbuf_cache=$(echo "$LIB_DIR"/gdk-pixbuf-*/*/loaders.cache) - dst_gdkpixbuf_cache=${lib%/*}.cache - if [ -f "$src_gdkpixbuf_cache" ] && [ ! -f "$dst_gdkpixbuf_cache" ]; then - cp -v "$src_gdkpixbuf_cache" "$dst_gdkpixbuf_cache" - sed -i -e 's|/usr/lib/.*/loaders/||g' "$dst_gdkpixbuf_cache" - _echo "* added $src_gdkpixbuf_cache" - fi - ;; - */gtk-*/*/immodules/*.so) - _gtkver=$(echo "$lib" | tr '/' '\n' | grep '^gtk-') - src_gtk_immodule_cache=$(echo "$LIB_DIR"/"$_gtkver"/*/immodules.cache) - dst_gtk_immodule_cache=${lib%/*}.cache - if [ -f "$src_gtk_immodule_cache" ] && [ ! -f "$dst_gtk_immodule_cache" ]; then - cp -v "$src_gtk_immodule_cache" "$dst_gtk_immodule_cache" - sed -i -e 's|/usr/lib/.*/immodules/||g' "$dst_gtk_immodule_cache" - _echo "* added $src_gtk_immodule_cache" - fi - ;; - */libglycin*.so*) - if [ "$GNOME_GLYCIN" != 1 ]; then - continue # only GNOME glycin needs handling - fi - _add_bwrap_wrapper - src_glycin_conf_dir=/usr/share/glycin-loaders - dst_glycin_conf_dir=$APPDIR/share/glycin-loaders - if [ -d "$src_glycin_conf_dir" ] && [ ! -d "$dst_glycin_conf_dir" ]; then - cp -r "$src_glycin_conf_dir" "$dst_glycin_conf_dir" - sed -i -e 's|/usr/lib.*/||g' "$dst_glycin_conf_dir"/*/*/*.conf - _echo "* added $src_glycin_conf_dir" - fi - ;; - */libgtksourceview-*.so*) - _gtk_srcview_ver=$(echo "$lib" | awk -F'-' '{print $NF}' | sed "s|\.so.*||") - src_gtk_srcview_dir=/usr/share/gtksourceview-$_gtk_srcview_ver - dst_gtk_srcview_dir=$APPDIR/share/gtksourceview-$_gtk_srcview_ver - if [ -d "$src_gtk_srcview_dir" ] && [ ! -d "$dst_gtk_srcview_dir" ]; then - cp -r "$src_gtk_srcview_dir" "$dst_gtk_srcview_dir" - _echo "* added $src_gtk_srcview_dir" - fi - ;; - */libfontconfig.so*) - src_fontconfig_config=/etc/fonts/fonts.conf - dst_fontconfig_config=$APPDIR/etc/fonts/fonts.conf - if [ -f "$src_fontconfig_config" ] && [ ! -f "$dst_fontconfig_config" ]; then - mkdir -p "${dst_fontconfig_config%/*}" - cp -v "$src_fontconfig_config" "$dst_fontconfig_config" - _echo "* added $src_fontconfig_config" - fi - ;; - */libfolks*.so*) - src_folks_dir=$LIB_DIR/folks - dst_folks_dir=$DST_LIB_DIR/folks - if [ -d "$src_folks_dir" ] && [ ! -d "$dst_folks_dir" ]; then - cp -r "$src_folks_dir" "$dst_folks_dir" - _echo "* added $src_folks_dir" - fi - ;; - */libthai*.so*) - src_libthai_dir=/usr/share/libthai - dst_libthai_dir=$APPDIR/share/libthai - if [ -d "$src_libthai_dir" ] && [ ! -d "$dst_libthai_dir" ]; then - cp -r "$src_libthai_dir" "$dst_libthai_dir" - _echo "* added $src_libthai_dir" - fi - ;; - */libasound*.so*) - src_alsaconf_dir=/usr/share/alsa - dst_alsaconf_dir=$APPDIR/share/alsa - if [ -d "$src_alsaconf_dir" ] && [ ! -d "$dst_alsaconf_dir" ]; then - cp -r "$src_alsaconf_dir" "$dst_alsaconf_dir" - _echo "* added $src_alsaconf_dir" - fi - # Adding alsa config dir is not enough, the file is harcoded - # to load additional files on the host - f=$APPDIR/share/alsa/alsa.conf - if [ -f "$f" ] && ! grep -q 'SHARUN_DIR' "$f"; then - sed -i -e \ - 's|"/etc/alsa/conf.d"|"/etc/alsa/conf.d"\n\t\t\t{ @func concat strings [ { @func getenv vars [ SHARUN_DIR ] default "" } "/share/alsa/alsa.conf.d" ] }|' \ - "$f" - fi - ;; - */libxkbcommon*.so*) - src_xkb_dir=/usr/share/X11/xkb - dst_xkb_dir=$APPDIR/share/X11/xkb - if [ -d "$src_xkb_dir" ] && [ ! -d "$dst_xkb_dir" ]; then - mkdir -p "$dst_xkb_dir" - cp -r "$src_xkb_dir"/* "$dst_xkb_dir" - _echo "* added $src_xkb_dir" - fi - ;; - */libX11.so*) - src_xlocale_dir=/usr/share/X11/locale - dst_xlocale_dir=$APPDIR/share/X11/locale - if [ -d "$src_xlocale_dir" ] && [ ! -d "$dst_xlocale_dir" ]; then - mkdir -p "$dst_xlocale_dir" - cp -r "$src_xlocale_dir"/* "$dst_xlocale_dir" - _echo "* added $src_xlocale_dir" - fi - ;; - */libgbm.so*) # This hook should never be hit since OpenGL deployment already handles this - src_gbm_backends_dir=$LIB_DIR/gbm - dst_gbm_backends_dir=$DST_LIB_DIR/gbm - if [ -d "$src_gbm_backends_dir" ] && [ ! -d "$dst_gbm_backends_dir" ]; then - cp -r "$src_gbm_backends_dir" "$dst_gbm_backends_dir" - _echo "* added $src_gbm_backends_dir" - fi - ;; - */libEGL_mesa.so*) - src_glvnd_dir=/usr/share/glvnd/egl_vendor.d - dst_glvnd_dir=$APPDIR/share/glvnd/egl_vendor.d - if [ -d "$src_glvnd_dir" ] && [ ! -d "$dst_glvnd_dir" ]; then - mkdir -p "$dst_glvnd_dir" - cp -v "$src_glvnd_dir"/*.json "$dst_glvnd_dir" - sed -i -e 's|/usr/lib.*/||g' "$dst_glvnd_dir"/*.json - _echo "* added $src_glvnd_dir" - fi - - src_drirc_dir=/usr/share/drirc.d - dst_drirc_dir=$APPDIR/share/drirc.d - if [ -d "$src_drirc_dir" ] && [ ! -d "$dst_drirc_dir" ]; then - cp -r "$src_drirc_dir" "$dst_drirc_dir" - _echo "* added $src_drirc_dir" - fi - ;; - */libdrm_amdgpu.so*) - src_libdrm_dir=/usr/share/libdrm - dst_libdrm_dir=$APPDIR/share/libdrm - if [ -d "$src_libdrm_dir" ] && [ ! -d "$dst_libdrm_dir" ]; then - cp -r "$src_libdrm_dir" "$dst_libdrm_dir" - _echo "* added $src_libdrm_dir" - fi - ;; - */libvulkan.so*) - src_vulkan_dir=/usr/share/vulkan/icd.d - dst_vulkan_dir=$APPDIR/share/vulkan/icd.d - if [ -d "$src_vulkan_dir" ] && [ ! -d "$dst_vulkan_dir" ]; then - mkdir -p "$dst_vulkan_dir" - cp -v "$src_vulkan_dir"/*.json "$dst_vulkan_dir" - sed -i -e 's|/usr/lib.*/||g' "$dst_vulkan_dir"/*.json - _echo "* added $src_vulkan_dir" - fi - ;; - */libVkLayer*.so*) - # find vulkan layer icd file - src_vklayer_icd=$(grep -r "${lib##*/}" /usr/share/vulkan/* | awk -F':' '{print $1; exit}') - dst_vklayer_icd=$APPDIR/${src_vklayer_icd#/usr/} - if [ -f "$src_vklayer_icd" ] && [ ! -f "$dst_vklayer_icd" ]; then - mkdir -p "${dst_vklayer_icd%/*}" - cp -vL "$src_vklayer_icd" "$dst_vklayer_icd" - sed -i -e 's|/usr/lib.*/||g' "$dst_vklayer_icd" - _echo "* added vulkan layer icd: $src_vklayer_icd" - fi - ;; - # this hook is a common false positive - # because a lot of applications execute commands thru the system shell - # and that often links to this library, causing overdeployment of terminfo files - */libncursesw.so*|*/libcursesw.so*|*/libcurses.so*) - src_terminfo_dir=/usr/share/terminfo - dst_terminfo_dir=$APPDIR/share/terminfo - if [ -d "$src_terminfo_dir" ] && [ ! -d "$dst_terminfo_dir" ]; then - cp -r "$src_terminfo_dir" "$dst_terminfo_dir" - _echo "* added $src_terminfo_dir" - fi - - src_tabset_dir=/usr/share/tabset - dst_tabset_dir=$APPDIR/share/tabset - if [ -d "$src_tabset_dir" ] && [ ! -d "$dst_tabset_dir" ]; then - cp -r "$src_tabset_dir" "$dst_tabset_dir" - _echo "* added $src_tabset_dir" - fi - ;; - */qt*/plugins/*.so) - f=$DST_BIN_DIR/qt.conf - if [ ! -f "$f" ]; then - _qtdir=${lib#$DST_LIB_DIR/} # leaves qt* - _qtdir=${_qtdir%%/*} # gets basename - _libdir=${DST_LIB_DIR##*/} # libdir basename (lib or lib32) - cat <<-EOF > "$f" - [Paths] - Prefix = ../$_libdir/$_qtdir - Plugins = plugins - Imports = qml - Qml2Imports = qml - EOF - _echo "* added $f " - fi - - # deploy translation files - src_qt_trans=/usr/share/$QT_DIR/translations - dst_qt_trans=$DST_LIB_DIR/$QT_DIR/translations - if [ -d "$src_qt_trans" ] && [ ! -d "$dst_qt_trans" ]; then - mkdir -p "${dst_qt_trans%/*}" - # debloat a bit since we don't need all of them - cp -r "$src_qt_trans" "$dst_qt_trans" - rm -f "$dst_qt_trans"/assistant*.qm - rm -f "$dst_qt_trans"/designer*.qm - rm -f "$dst_qt_trans"/linguist*.qm - _echo "* added $src_qt_trans" - fi - ;; - */libgs.so*) - src_gs_dir=/usr/share/ghostscript - dst_gs_dir=$APPDIR/share/ghostscript - if [ -d "$src_gs_dir" ] && [ ! -d "$dst_gs_dir" ]; then - cp -r "$src_gs_dir" "$dst_gs_dir" - _echo "* added $src_gs_dir" - fi - ;; - */libmagic.so*) - # sharun only checks for $SHARUN_DIR/share/file/misc/magic.mgc - # but on ubuntu for example, the file is located in /usr/share/file/magic.mgc - # so we need to find the magic.mgc file and copy it to dst - src_magic_file=$(find -L /usr/share/file -type f -name magic.mgc -print -quit) || : - dst_magic_file=$APPDIR/share/file/misc/magic.mgc - if [ -f "$src_magic_file" ] && [ ! -f "$dst_magic_file" ]; then - mkdir -p "${dst_magic_file%/*}" - cp -vL "$src_magic_file" "$dst_magic_file" - _echo "* added $src_magic_file" - fi - ;; - */libgirepository-*.so*) - _girver=$(echo "$lib" | awk -F'-' '{print $NF}' | sed "s|\.so.*||") - src_girepository_dir=$LIB_DIR/girepository-$_girver - dst_girepository_dir=$DST_LIB_DIR/girepository-$_girver - if [ -d "$src_girepository_dir" ] && [ ! -d "$dst_girepository_dir" ]; then - cp -r "$src_girepository_dir" "$dst_girepository_dir" - _echo "* added $src_girepository_dir" - - # there might be more .typelib files around, we need to copy them - _typelibfiles=$(find "$LIB_DIR"/*/* -type f -name '*.typelib' 2>/dev/null \ - | grep -v "$src_girepository_dir" | grep girepository-"$_girver" - ) || : - for f in $_typelibfiles; do - [ -f "$f" ] || continue - cp -v "$f" "$dst_girepository_dir" - done - if [ -n "$_typelibfiles" ]; then - _echo "* added additional .typelib files" - fi - fi - ;; - */gconv/*.so) - src_gconvm_file=$LIB_DIR/gconv/gconv-modules - dst_gconvm_file=$DST_LIB_DIR/gconv/gconv-modules - if [ -f "$src_gconvm_file" ] && [ ! -f "$dst_gconvm_file" ]; then - mkdir -p "${dst_gconvm_file%/*}" - cp -v "$src_gconvm_file" "$dst_gconvm_file" - _echo "* added $src_gconvm_file" - fi - ;; - */libc.so*) - src_c_locale_dir=/usr/lib/locale/C.utf8 - dst_c_locale_dir=$DST_LIB_DIR/locale/C.utf8 - mkdir -p "$DST_LIB_DIR"/locale - if [ -d "$src_c_locale_dir" ] && [ ! -d "$dst_c_locale_dir" ]; then - cp -r "$src_c_locale_dir" "$dst_c_locale_dir" - _echo "* added C.UTF-8 locale" - fi - # C.UTF-8 is not enough, some apps may crash when this locale is used - # so we need to ship en_US.UTF-8 so we can guarantee applications - # will launch in systems without glibc locales like alpine linux - # - # Because distros use a locale-archive these days, we have to compile it - # - dst_en_locale_dir=$DST_LIB_DIR/locale/en_US.utf8 - if [ ! -d "$dst_en_locale_dir" ] && _is_cmd localedef; then - mkdir -p /tmp/usr/lib/locale - localedef --prefix /tmp --no-archive -i en_US -f UTF-8 en_US.UTF-8 || : - if cp -r /tmp/usr/lib/locale/en_US.utf8 "$DST_LIB_DIR"/locale; then - _echo "* added en_US.UTF-8 locale" - fi - fi - ;; - */ld-linux*.so*|*/ld-musl*.so*) - # patch away the dynamic linker /etc to disable /etc/ld.so.preload - if grep -qa -m 1 '/etc' "$lib"; then - sed -i -e 's|/etc/ld.so.preload|/XXX/ld.so.preload|g' "$lib" - _echo "* patched away ${lib##*/} /etc/ld.so.preload" - fi - ;; - */libgegl*.so*) - src_gegl_dir=$(echo "$LIB_DIR"/gegl-*) - dst_gegl_dir=$DST_LIB_DIR/${src_gegl_dir##*/} - if [ -d "$src_gegl_dir" ] && [ -d "$dst_gegl_dir" ]; then - if cp "$src_gegl_dir"/*.json "$dst_gegl_dir"; then - _echo "* added $src_gegl_dir .json files" - fi - fi - # GEGL_PATH is problematic so we avoid it - # patch the lib directly to load its plugins instead - _patch_away_usr_lib_dir "$lib" || continue - echo 'unset GEGL_PATH' >> "$APPENV" - ;; - */libMagick*.so*) - src_magick_config_dir=$(echo /etc/ImageMagick*) - dst_magick_config_dir=$APPDIR/etc/${src_magick_config_dir##*/} - if [ -d "$src_magick_config_dir" ] && [ ! -d "$dst_magick_config_dir" ]; then - mkdir -p "$dst_magick_config_dir" - ( - # imagemagick has a ton of .xml config files that need - # to be added, they can all be copied to one location - set -- \ - /usr/share/ImageMagick*/* \ - "$src_magick_config_dir"/* \ - "$LIB_DIR"/ImageMagick*/config*/*.xml - for f do - if [ -f "$f" ]; then - _copy=1 - cp "$f" "$dst_magick_config_dir" - fi - done - if [ "$_copy" = 1 ]; then - _echo "* added ImageMagick config files..." - fi - # MAGICK_HOME is all that needs to be set - echo 'MAGICK_HOME=${SHARUN_DIR}' >> "$APPENV" - # however MAGICK_HOME only works when compiled with a specific flag - # we can still make this relocatable by setting these other env variables - # which will always work even when not compiled with MAGICK_HOME support - cd "$APPDIR" - set -- lib*/ImageMagick-*/modules*/coders - if [ -d "$1" ]; then - echo "MAGICK_CODER_MODULE_PATH=\${SHARUN_DIR}/$1" >> "$APPENV" - fi - set -- lib*/ImageMagick-*/modules*/filters - if [ -d "$1" ]; then - # checking the code it seems that MAGICK_FILTER_MODULE_PATH - # is NOT USED in the code and seems to be an error!!! the variable - # that modules.c references is MAGICK_CODER_FILTER_PATH - # we will still be set both just in case - echo "MAGICK_CODER_FILTER_PATH=\${SHARUN_DIR}/$1" >> "$APPENV" - echo "MAGICK_FILTER_MODULE_PATH=\${SHARUN_DIR}/$1" >> "$APPENV" - fi - set -- etc/ImageMagick* - if [ -d "$1" ]; then - echo "MAGICK_CONFIGURE_PATH=\${SHARUN_DIR}/$1" >> "$APPENV" - fi - ) - fi - ;; - */libp11-kit.so*) - src_p11kit_config_dir=/usr/share/p11-kit - dst_p11kit_config_dir=$APPDIR/share/p11-kit - if [ -d "$src_p11kit_config_dir" ] && [ ! -d "$dst_p11kit_config_dir" ]; then - cp -r "$src_p11kit_config_dir" "$dst_p11kit_config_dir" - _echo "* added $src_p11kit_config_dir" - fi - _patch_away_usr_lib_dir "$lib" || : - _patch_away_usr_share_dir "$lib" || : - ;; - */p11-kit-trust.so*) - # Because OpenSUSE had to ruin this, we will have to patch the - # the certificates to a path in /tmp that we will later make - # a symlink that points to the real host certs location - - # Originally we just patch to etc/ssl/certs/ca-certificates.crt - # See https://github.com/kem-a/AppManager/issues/39 - - # string has to be same length - problem_path="/usr/share/ca-certificates/trust-source" - ssl_path_fix="/tmp/.___host-certs/ca-certificates.crt" - - if grep -Eaoq -m 1 "$ssl_path_fix" "$lib"; then - continue # all good nothing to fix - elif grep -Eaoq -m 1 "$problem_path" "$lib"; then - sed -i -e "s|$problem_path|$ssl_path_fix|g" "$lib" - else - continue # TODO add more possible problematic paths - fi - - _add_p11kit_cert_hook - - _echo "* fixed path to /etc/ssl/certs in $lib" - _patch_away_usr_share_dir "$lib" || continue - ;; - */libcrypto.so*) - # Apps may fail to connect to internet if they use the host ssl config - # see: https://github.com/pkgforge-dev/Viber-AppImage-Enhanced/issues/16 - dst_ssl_conf=$APPDIR/etc/ssl/openssl.cnf - if [ ! -f "$dst_ssl_conf" ]; then - mkdir -p "${dst_ssl_conf%/*}" - # make a minimal ssl config instead of copying the hosts - cat <<-'EOF' > "$dst_ssl_conf" - [openssl_conf] - openssl_conf = openssl_init - - [openssl_init] - providers = provider_sect - - [provider_sect] - default = default_sect - - [default_sect] - activate = 1 - EOF - _echo "* added minimal ssl config" - fi - ;; - */libgimpwidgets*) - _patch_away_usr_share_dir "$lib" || continue - ;; - */libmlt*.so*) - src_mlt_data_dir=$(echo /usr/share/mlt-*) - dst_mlt_data_dir=$APPDIR/share/${src_mlt_data_dir##*/} - - if [ -d "$src_mlt_data_dir" ] && [ ! -d "$dst_mlt_data_dir" ]; then - cp -r "$src_mlt_data_dir" "$dst_mlt_data_dir" - _echo "* added $src_mlt_data_dir" - fi - ;; - */libMangoHud*.so*) - src_mangohud_layer=$(echo /usr/share/vulkan/implicit_layer.d/MangoHud*.json) - dst_mangohud_layer="$APPDIR"/share/vulkan/implicit_layer.d/"${src_mangohud_layer##*/}" - if [ -f "$src_mangohud_layer" ] && [ ! -f "$dst_mangohud_layer" ]; then - mkdir -p "$APPDIR"/share/vulkan/implicit_layer.d - cp -v "$src_mangohud_layer" "$dst_mangohud_layer" - sed -i 's|/.*/mangohud/||' "$dst_mangohud_layer" - - if [ ! -f "$DST_BIN_DIR"/mangohud ] \ - && command -v mangohud 1>/dev/null; then - cp -v "$(command -v mangohud)" "$DST_BIN_DIR" - fi - - sed -i \ - -e 's|/usr/.*/||' \ - -e '1a\export SHARUN_ALLOW_LD_PRELOAD=1' \ - -e 's|#!.*|#!/bin/sh|' \ - "$DST_BIN_DIR"/mangohud || : - - _echo "Copied over mangohud layer and patched mangohud" - fi - ;; - */libwebkit*gtk-*.so*) - _add_bwrap_wrapper - # now do better path map to the libs - _patch_away_usr_lib_dir "$lib" || : - _patch_away_usr_bin_dir "$lib" || : - - # check if webkit2gtk was compiled relocatable - if grep -aq -m 1 'WEBKIT_EXEC_PATH' "$lib" \ - && ! grep -q WEBKIT_EXEC_PATH "$APPENV"; then - ( - set -- "$DST_BIN_DIR"/WebKit* - if [ -f "$1" ]; then - echo 'WEBKIT_EXEC_PATH=${SHARUN_DIR}/bin' >> "$APPENV" - fi - ) - fi - ;; - */libwebkit*gtkinjectedbundle.so*) - # WEBKIT_INJECTED_BUNDLE_PATH always works - # It is not guarded behind a compiled flag unlike WEBKIT_EXEC_PATH - if ! grep -q 'WEBKIT_INJECTED_BUNDLE_PATH' "$APPENV"; then - cp -v "$lib" "$DST_BIN_DIR" - echo 'WEBKIT_INJECTED_BUNDLE_PATH=${SHARUN_DIR}/bin' >> "$APPENV" - fi - ;; - */libdecor*.so*) - ADD_HOOKS="${ADD_HOOKS:+$ADD_HOOKS:}fix-gnome-csd.hook" - ;; - */libSDL*.so*) - # SDL may be bundled without libdecor since it maybe missing from the CI runner - # or the application makes of GTK/Qt + SDL, in which case we do not need libdecor - # at all, make sure SDL does not attempt to load libdecor in these cases - if [ -f "$DST_LIB_DIR"/libdecor-0.so.0 ]; then - continue - elif grep -aoq -m 1 'libdecor-0.so.0' "$lib"; then - sed -i -e 's|libdecor-0.so.0|fuck-gnome.so.X|g' "$lib" - fi - ;; - */xpm.so) - f=/usr/share/imlib2/rgb.txt - if [ -f "$f" ]; then - mkdir -p "$APPDIR"/share/imlib2 - cp -v "$f" "$APPDIR"/share/imlib2 - _patch_away_usr_share_dir "$lib" || continue - _echo "Copied and patched imlib2 xpm loader" - fi - ;; - */7z.so) - # the 7z binaries need the lib next to them - cp -v "$lib" "$DST_BIN_DIR" - ;; - */libpipewire-*.so*) - src_pipewire_config_dir=/usr/share/pipewire - dst_pipewire_config_dir=$APPDIR/share/pipewire - if [ -d "$src_pipewire_config_dir" ] && [ ! -d "$dst_pipewire_config_dir" ]; then - cp -r "$src_pipewire_config_dir" "$dst_pipewire_config_dir" - - cat <<-'EOF' > "$DST_BIN_DIR"/01-pipewire-config.hook - _pipewire_dir=$APPDIR/share/pipewire - if [ ! -d /usr/share/pipewire ] && [ -d "$_pipewire_dir" ]; then - export PIPEWIRE_CONFIG_DIR="$_pipewire_dir" - fi - EOF - fi - ;; - */libtesseract.so*) - src_tess_data_dir=/usr/share/tessdata - dst_tess_data_dir=$APPDIR/share/tessdata - if [ -d "$src_tess_data_dir" ] && [ ! -d "$dst_tess_data_dir" ]; then - cp -r "$src_tess_data_dir" "$dst_tess_data_dir" - _echo "* added $src_tess_data_dir" - fi - ;; - esac -done - -_deploy_datadir - -# copy the entire hicolor icons dir -# by default the hicolor icon theme ships no icons, this -# means any present icon is likely needed by the application -if [ -d /usr/share/icons/hicolor ]; then - mkdir -p "$APPDIR"/share/icons - cp -r /usr/share/icons/hicolor "$APPDIR"/share/icons - _remove_empty_dirs "$APPDIR"/share/icons/hicolor -fi - -_deploy_locale - -# make the lib.path file. Very important for sharun to discover bundled libs! -"$APPDIR"/sharun -g - -# on debian some libs may hardcode paths like /usr/lib/x86_64-linux-gnu -# make a compat symlink so patched paths resolve to bundled libs -d=$APPIMAGE_ARCH-linux-gnu -case "$LIB_DIR" in - */"$d"*) - ( cd "$DST_LIB_DIR" && ln -s . "$d" 2>/dev/null || : ) - ;; -esac - -_strip_bins_and_libs -_check_hardcoded_lib_dirs -_check_hardcoded_data_dirs - -# patch away any hardcoded path to /usr/share or /usr/lib in bins... -set -- "$SHARUN_BIN_DIR"/* -for bin do - if p=$(grep -ao -m 1 '/usr/share/.*/' "$bin"); then - _echo "* Detected hardcoded path to $p in $bin" - _patch_away_usr_share_dir "$bin" || : - fi - if p=$(grep -ao -m 1 '/usr/lib/.*/' "$bin"); then - _echo "* Detected hardcoded path to $p in $bin" - _patch_away_usr_lib_dir "$bin" || : - fi - if _is_bun_binary "$bin" || _is_pyinstaller_binary "$bin"; then - # bun/pyisntaller binaries cannot be executed with the - # dynamic linker directly, so we will change PT_INTERP to - # /tmp/.ld-sharun.so.67, sharun will copy it there at runtime - patchelf --set-interpreter /tmp/.ld-sharun.so.67 "$bin" - _echo "* Set interpreter to /tmp/.ld-sharun.so.67 for $bin" - fi -done - -echo "" -_echo "------------------------------------------------------------" -echo "" - -if [ -n "$ADD_HOOKS" ]; then - old_ifs="$IFS" - IFS=':' - set -- $ADD_HOOKS - IFS="$old_ifs" - hook_dst=$DST_BIN_DIR - for hook do - # hooks used to be executed differently depending on the suffix - # this was dropped and now all hooks are sourced - # remove old suffixes so that we don't break existing scripts - hook=${hook%.bg.hook} - hook=${hook%.src.hook} - # also remove .hook before adding it again - # this allows declaring a hook without the suffix in ADD_HOOKS - hook=${hook%.hook} - hook=${hook}.hook - - if [ -f "$hook_dst"/"$hook" ]; then - continue - elif _download "$hook_dst"/"$hook" "$HOOKSRC"/"$hook"; then - _echo "* Added $hook" - else - _err_msg "ERROR: Failed to download $hook, valid link?" - _err_msg "$HOOKSRC/$hook" - exit 1 - fi - done -fi - -_add_hooks_library -_add_apprun - -chmod +x "$APPDIR"/AppRun.sh "$APPDIR"/AppRun || : - -# deploy directories -while read -r d; do - if [ -d "$d" ]; then - case "$d" in - "$LIB_DIR"/*) - if [ "$LIB32" = 1 ]; then - dst_path="$APPDIR"/lib32/"${d##*$LIB_DIR/}" - else - dst_path="$APPDIR"/lib/"${d##*$LIB_DIR/}" - fi - ;; - */share/*) - dst_path="$APPDIR"/share/"${d##*/share/}" - ;; - */etc/*) - dst_path="$APPDIR"/etc/"${d##*/etc/}" - ;; - */lib/*) - dst_path="$APPDIR"/lib/"${d##*/lib/}" - ;; - */lib32/*) - dst_path="$APPDIR"/lib32/"${d##*/lib32/}" - ;; - "$APPDIR"/*|./"${APPDIR##*/}"/*|"${APPDIR##*/}"/*) - _err_msg "Skipping deployment of $d (already in '$APPDIR')" - continue - ;; - *) - _err_msg "Skipping deployment of $d" - _err_msg "Valid directories to deploy are:" - _err_msg "Any dir from: $LIB_DIR" - _err_msg "Any dir with /lib/ in its path" - _err_msg "Any dir with /share/ in its path" - _err_msg "Any dir with /etc/ in its path" - continue - ;; - esac - mkdir -p "${dst_path%/*}" - if cp -Lrn "$d"/. "$dst_path"; then - _echo "* Added $d to $dst_path" - else - # do not stop the script if the copy fails, because - # since lib4bin skips directories automatically we do - # not want CIs to fail because suddenly now we are - # trying to copy some directory that we did not have - # read access to that lib4bin was previously skipping - _err_msg "Failed to add $d to $dst_path/${d##*/}" - fi - fi -done <<-EOF -$ADD_DIR -EOF - -_handle_nested_bins - -if [ -n "$ANYLINUX_DO_NOT_LOAD_LIBS" ]; then - echo "ANYLINUX_DO_NOT_LOAD_LIBS=$ANYLINUX_DO_NOT_LOAD_LIBS:\${ANYLINUX_DO_NOT_LOAD_LIBS}" >> "$APPENV" -fi - -# check if we have libjack.so in the AppImage, jack needs matching -# server and client library versions to work, instead we need to use -# pipewire-jack, which gives a libjack.so that does not have this limitation -libjackwarning=" ------------------------------------------------------------- ------------------------------------------------------------- - -WARNING: Detected libjack.so has been bundled in this application! -If this app is going to connect to a jack server it is not going to work! -jack needs matching library versions between clients and server to work! - -The only solution is bundling libjack.so from pipewire-jack -package instead which does not have this issue. - -NOTE: This is only a problem if the application has the option to connect -to a jack server, that is for example music players and music editing software -libjack.so can be bundled as linked dependency of another library like -ffmpeg and in that case this is not an issue. - ------------------------------------------------------------- ------------------------------------------------------------- -" -set -- "$DST_LIB_DIR"/libjack.so* -if [ -f "$1" ]; then - if ! ldd "$1" | grep -q 'libpipewire'; then - _err_msg "$libjackwarning" - fi -fi - -# also warn when several common qt theme plugins are missing, we only do this for qt6 -if [ -d "$DST_LIB_DIR"/qt6 ]; then - for p in kvantum qtlxqt qt6ct; do - set -- "$DST_LIB_DIR"/qt6/plugins/*/*$p* - if [ ! -f "$1" ]; then - _err_msg "------------------------------------------------------------" - _err_msg "WARNING: Qt was deployed but there is no $p plugin!" - _err_msg "This means the application will lack proper theme support!" - _err_msg "Install the packages that provide theme support before deploying" - _err_msg "In archlinux those are: qt6ct kvantum lxqt-qtplugin" - _err_msg "------------------------------------------------------------" - fi - done -fi - -# suggest people to use glycin-ng instead -if [ "$GNOME_GLYCIN" = 1 ]; then - _err_msg "------------------------------------------------------------" - _err_msg "WARNING: GNOME glycin has been deployed!" - _echo "There is a much better alternative called glycin-ng, features include:" - _echo "* 5 times smaller!" - _echo "* No bwrap dependency (uses landlock for sandbox instead)" - _echo "* No dbus dependency" - _echo "https://github.com/QaidVoid/glycin-ng" - _err_msg "------------------------------------------------------------" -fi - -echo "" -if [ "$OUTPUT_APPIMAGE" = 1 ]; then - _make_appimage -else - _sort_env_file - _echo "------------------------------------------------------------" - _echo "All done!" - _echo "------------------------------------------------------------" -fi diff --git a/CMakeLists.txt b/CMakeLists.txt index b28469b284..a7dbc06865 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -604,17 +604,7 @@ if (ENABLE_QT AND NOT CITRON_USE_CPM) message(STATUS "Using host Qt at ${QT_HOST_PATH}") endif() - # Early Qt6 discovery: find Widgets and Concurrent unconditionally; add - # Multimedia only when CITRON_USE_QT_MULTIMEDIA is ON. This makes the - # cmake flag actually functional: with CITRON_USE_QT_MULTIMEDIA=OFF the - # Multimedia module and its shared FFmpeg backend (libffmpegmediaplugin.so, - # libavcodec.so.61 ~80 MB, etc.) are not linked at all. - set(_qt_probe_components Widgets Concurrent) - if (CITRON_USE_QT_MULTIMEDIA) - list(APPEND _qt_probe_components Multimedia) - endif() - find_package(Qt6 REQUIRED COMPONENTS ${_qt_probe_components}) - unset(_qt_probe_components) + find_package(Qt6 REQUIRED COMPONENTS Widgets Multimedia Concurrent) if (UNIX AND NOT APPLE) find_package(Qt6 REQUIRED COMPONENTS DBus) diff --git a/CMakeModules/qt_download.cmake b/CMakeModules/qt_download.cmake index 15768b8a22..063a8eab0e 100644 --- a/CMakeModules/qt_download.cmake +++ b/CMakeModules/qt_download.cmake @@ -9,8 +9,7 @@ # # Target variants: # Windows (native or cross-compile target) → win64_llvm_mingw -# Linux native x86-64 → linux_gcc_64 (via aqt) -# Linux native aarch64 → linux_gcc_arm64 (host: linux_arm64) +# Linux native → linux_gcc_64 # # Cross-compilation (Linux host → Windows target): # QT_HOST_PATH is set to a Linux Qt install so moc/rcc/uic run on the host. @@ -71,22 +70,12 @@ else() set(_QT_DIR_NAME "macos") set(_QT_CMAKE_SUB "lib/cmake/Qt6") else() - # Native Linux — pick host/arch variant to match the build processor so - # moc/rcc/uic (which run on the build host) are the correct ELF arch. - # aqt uses separate host-OS strings for x86-64 ("linux") and arm64 - # ("linux_arm64"); the arch token is then linux_gcc_64 or linux_gcc_arm64. + # Native Linux + set(_QT_OS "linux") + set(_QT_TARGET "desktop") + set(_QT_ARCH "linux_gcc_64") + set(_QT_DIR_NAME "gcc_64") set(_QT_CMAKE_SUB "lib/cmake/Qt6") - if (CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "aarch64|arm64" OR ARCHITECTURE_arm64) - set(_QT_OS "linux_arm64") - set(_QT_TARGET "desktop") - set(_QT_ARCH "linux_gcc_arm64") - set(_QT_DIR_NAME "gcc_arm64") - else() - set(_QT_OS "linux") - set(_QT_TARGET "desktop") - set(_QT_ARCH "linux_gcc_64") - set(_QT_DIR_NAME "gcc_64") - endif() endif() endif() @@ -123,29 +112,23 @@ else() message(STATUS "[Qt] Qt ${CITRON_QT_VERSION} target downloaded") endif() - # Download additional modules (imageformats, svg, tools). - # qtmultimedia is intentionally excluded: all Qt Multimedia camera code in - # citron is guarded by (QT_VERSION < 6.0.0) && CITRON_USE_QT_MULTIMEDIA, - # so it is permanently dead code under Qt6. CITRON_USE_QT_MULTIMEDIA=OFF - # is set in build-citron-linux.sh, but the Qt5-only guard makes it a no-op - # regardless. Including qtmultimedia in --modules was the sole cause of the - # recurring "[Qt] Additional module install failed" CI warning: aqt silently - # rejects module name mismatches for certain arch/version combos. + # Download additional modules (multimedia, imageformats, svg) + set(_QT_MM_CMAKE "${_QT_TARGET_DIR}/lib/cmake/Qt6Multimedia/Qt6MultimediaConfig.cmake") set(_QT_SVG_CMAKE "${_QT_TARGET_DIR}/lib/cmake/Qt6Svg/Qt6SvgConfig.cmake") set(_QT_TOOL_CMAKE "${_QT_TARGET_DIR}/lib/cmake/Qt6CoreTools/Qt6CoreToolsConfig.cmake") - if (NOT EXISTS "${_QT_SVG_CMAKE}" OR NOT EXISTS "${_QT_TOOL_CMAKE}") - message(STATUS "[Qt] Downloading Qt ${CITRON_QT_VERSION} additional modules (qttools, qtimageformats)...") + if (NOT EXISTS "${_QT_MM_CMAKE}" OR NOT EXISTS "${_QT_SVG_CMAKE}" OR NOT EXISTS "${_QT_TOOL_CMAKE}") + message(STATUS "[Qt] Downloading Qt ${CITRON_QT_VERSION} additional modules...") execute_process( COMMAND ${_AQT_EXECUTABLE} install-qt ${_QT_OS} ${_QT_TARGET} ${CITRON_QT_VERSION} ${_QT_ARCH} --outputdir "${CITRON_QT_BASE_DIR}" - --modules qttools qtimageformats - RESULT_VARIABLE _qt_addl_result + --modules qttools qtmultimedia qtimageformats + RESULT_VARIABLE _qt_mm_result OUTPUT_QUIET ERROR_QUIET ) - if (NOT _qt_addl_result EQUAL 0) - message(WARNING "[Qt] Additional module install failed (qttools/qtimageformats) — build may fail") + if (NOT _qt_mm_result EQUAL 0) + message(WARNING "[Qt] Additional module install failed — build may fail") endif() endif() @@ -189,7 +172,14 @@ if (CMAKE_HOST_UNIX AND WIN32) if (NOT _qt_host_result EQUAL 0) message(WARNING "[Qt] Host Qt download failed — cross-compile may fail") endif() - # qtmultimedia not downloaded for host Qt — dead code under Qt6 (see target Qt comment above) + + execute_process( + COMMAND ${_AQT_EXECUTABLE} install-qt linux desktop + ${CITRON_QT_VERSION} linux_gcc_64 + --outputdir "${CITRON_QT_BASE_DIR}" + --modules qtmultimedia + OUTPUT_QUIET ERROR_QUIET + ) endif() if (EXISTS "${_QT_HOST_CMAKE}") diff --git a/build-citron-linux.sh b/build-citron-linux.sh index d81450d975..251aea81d1 100755 --- a/build-citron-linux.sh +++ b/build-citron-linux.sh @@ -123,7 +123,6 @@ # autoconf + automake + make FFmpeg autotools build # glslang (glslc) Vulkan shader compilation # patchelf bundle RPATH normalization -# gamemode bundled into AppImage if present (package stage) # ============================================================================= set -euo pipefail @@ -147,7 +146,6 @@ LTO_MODE="${LTO_MODE:-full}" PGO_MODE="${PGO_MODE:-ir}" UNITY_BUILD="${UNITY_BUILD:-OFF}" BUILD_TYPE="${BUILD_TYPE:-Release}" -NO_PACKAGE="${NO_PACKAGE:-false}" 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 @@ -382,117 +380,9 @@ _setup_apt() { glslang-tools \ patchelf \ lsb-release software-properties-common gnupg \ - libelf-dev libssl-dev libzstd-dev libudev-dev zstd \ - libgl-dev libopengl-dev \ - libxkbcommon-dev - # linux-tools-common/generic are best-effort: the versioned - # linux-tools-$(uname -r) package frequently doesn't exist on the runner's - # kernel and is intentionally optional. Keeping them in a separate call - # (not grouped with the mandatory packages above) ensures a missing - # perf/tools package never masks a failure in the required deps. - sudo apt-get install -y linux-tools-common linux-tools-generic 2>/dev/null || true + 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 - # NOTE: libgl-dev and libopengl-dev are MANDATORY even though citron is - # Vulkan-only and never calls any OpenGL API at runtime. The aqt-downloaded - # Qt6 (linux_gcc_64) was built against the GLVND OpenGL interface, so its - # Qt6GuiConfig.cmake requires cmake's WrapOpenGL module to find - # libOpenGL.so (from libopengl-dev) and libGL.so / GL headers (libgl-dev) - # at configure time. Without them find_package(Qt6 ... Widgets) fails: - # Could NOT find WrapOpenGL → Qt6Gui NOT FOUND → Qt6Widgets NOT FOUND - # Both packages are tiny (~200 KB combined) and must not be in the optional - # block below, which can fail atomically and be silently skipped. - - # ── Hardware video acceleration for bundled FFmpeg ────────────────────── - # externals/ffmpeg/CMakeLists.txt uses pkg_check_modules to detect VAAPI, - # VDPAU, and NVDEC, then conditionally enables them in FFmpeg's configure - # script. Missing packages cause graceful fallback (--disable-vaapi etc.), - # EXCEPT for the packages that are REQUIRED inside the LIBVA_FOUND block: - # - # if(LIBVA_FOUND) - # pkg_check_modules(LIBDRM libdrm REQUIRED) ← hard-fail if absent - # find_package(X11 REQUIRED) ← hard-fail if absent - # pkg_check_modules(LIBVA-DRM libva-drm REQUIRED) - # pkg_check_modules(LIBVA-X11 libva-x11 REQUIRED) - # - # Additionally, SDL2's cmake (also CPM-sourced) runs CheckX11() whenever - # libX11.so.6 is findable on the system. Inside CheckX11(), SDL2 2.x - # unconditionally hard-fails if Xext.h is absent: - # - # message_error("*** ERROR: Missing Xext.h, maybe you need to - # install the libxext-dev package?") - # - # This means libxext-dev MUST always be installed alongside libx11-dev. - # Installing libx11-dev without libxext-dev triggers the SDL2 hard-fail. - # - # These cascading REQUIRED constraints make libva-dev, libdrm-dev, - # libx11-dev, and libxext-dev an all-or-nothing group: if libva-dev - # installs but any of its sibling REQUIRED packages don't, cmake - # hard-fails the build. APT's atomic install behaviour (the whole command - # either succeeds or is rolled back) is what keeps this safe. - # DO NOT use --ignore-missing on this group, and DO NOT split it. - - # ── VAAPI + X11 core (all-or-nothing) ─────────────────────────────────── - # libva-dev → libva.pc, libva-drm.pc, libva-x11.pc - # libva-drm2 → runtime libva-drm.so (linked by FFmpeg) - # libva-x11-2 → runtime libva-x11.so - # libdrm-dev → libdrm.pc (REQUIRED by FFmpeg cmake when LIBVA_FOUND) - # libx11-dev → X11 headers (REQUIRED by FFmpeg cmake when LIBVA_FOUND) - # libxext-dev → Xext.h (REQUIRED by SDL2 cmake whenever libX11.so.6 is - # findable, regardless of VAAPI; must travel with libx11-dev) - info "Installing VAAPI + X11 core packages (required together)..." - sudo apt-get install -y \ - libva-dev libva-drm2 libva-x11-2 \ - libdrm-dev \ - libx11-dev libxext-dev \ - || warn "VAAPI+X11 group install failed — FFmpeg will build with --disable-vaapi and SDL2 without X11" - - # ── VDPAU (NVIDIA legacy — independent of VAAPI) ───────────────────────── - # libvdpau-dev → vdpau.pc; cmake detects independently of libva - info "Installing VDPAU hw-accel packages..." - sudo apt-get install -y libvdpau-dev \ - || warn "libvdpau-dev unavailable — FFmpeg will build with --disable-vdpau" - - # ── Linux audio output (ALSA + PulseAudio) — REQUIRED, not optional ───── - # SDL2's CheckALSA()/CheckPulseAudio() configure-time checks look for - # alsa/asoundlib.h and pulse/pulseaudio.h. If either header is missing, - # SDL2 silently disables that backend (SDL_ALSA / SDL_PULSEAUDIO → OFF) - # rather than hard-failing the build — the same "quiet fallback" pattern - # as the VAAPI/VDPAU checks above, except here the fallback is SDL2's - # SDL_DUMMYAUDIO/SDL_DISKAUDIO drivers, neither of which produces any - # actual sound. A machine missing both dev packages therefore still - # builds and packages successfully, but ships an AppImage with no - # functioning Linux audio output at all — a failure mode with no build - # warning to catch it, which is why this group is REQUIRED (unlike the - # --ignore-missing X11/XCB extras below). - # - # This also solves packaging determinism: package-citron-linux.sh finds - # libasound.so.2 and libpulse.so.0 to bundle into the AppImage via - # `ldconfig -p` against whatever happens to be installed on the machine - # running the packaging step (see comments there). Installing the -dev - # packages here pulls in their runtime libs (libasound2, libpulse0) as - # apt dependencies, so that probe now succeeds identically on every - # machine that runs this script — CI included — instead of depending on - # whether that machine happens to already have a desktop audio stack. - info "Installing ALSA + PulseAudio dev packages (required for audio output)..." - sudo apt-get install -y libasound2-dev libpulse-dev \ - || error "ALSA/PulseAudio dev packages failed to install — Linux builds would have no audio output" - - # ── X11 / XCB optional extras (SDL2 Xi/XSS/XCB, Qt XCB platform plugin) ─ - # These extend the X11 install above with input, screensaver, and XCB - # extension headers. All are optional — their cmake checks produce soft - # warnings, not hard errors. --ignore-missing lets a single unavailable - # package name (which can vary across distro versions) skip cleanly. - info "Installing optional X11/XCB extension packages..." - sudo apt-get install -y --ignore-missing \ - libxi-dev \ - libxkbcommon-x11-dev libxss-dev \ - libxcb1-dev libxcb-cursor-dev libxcb-image0-dev \ - libxcb-render-util0-dev libxinerama-dev \ - libgles-dev \ - || warn "Some optional X11/XCB extension packages unavailable — optional display features may be limited" - - # Optional: bundled into the AppImage by package-citron-linux.sh if present. - sudo apt-get install -y gamemode 2>/dev/null || true } _setup_pacman() { @@ -502,34 +392,8 @@ _setup_pacman() { python python-pip curl wget \ nasm yasm perl \ autoconf automake make \ - glslang clang lld llvm zstd \ + glslang clang lld llvm \ patchelf perf 2>/dev/null || true - - # Hardware video acceleration for bundled FFmpeg (VAAPI / VDPAU) - # X11/XCB libraries required by SDL2 and Qt - sudo pacman -S --needed --noconfirm \ - libva \ - libva-utils \ - libdrm \ - libvdpau \ - libx11 \ - libxext \ - libxi \ - libxkbcommon-x11 \ - libxss \ - libxcb \ - libxcb-cursor \ - libxcb-image \ - libxcb-render-util \ - libxinerama 2>/dev/null || warn "Hardware acceleration libraries unavailable — FFmpeg will be software-decode only" - - # Optional: bundled into the AppImage by package-citron-linux.sh if present. - sudo pacman -S --needed --noconfirm gamemode 2>/dev/null || true - - # ── Linux audio output (ALSA + PulseAudio) — - sudo pacman -S --needed --noconfirm alsa-lib libpulse \ - || error "ALSA/PulseAudio packages failed to install — Linux builds would have no audio output" - # 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}" @@ -548,34 +412,10 @@ _setup_dnf() { nasm yasm perl \ autoconf automake make \ glslang clang lld patchelf \ - elfutils-libelf-devel openssl-devel libudev-devel zstd \ + 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." - - # Hardware video acceleration for bundled FFmpeg (VAAPI / VDPAU) - # X11/XCB libraries required by SDL2 and Qt - sudo dnf install -y \ - libva-devel \ - libdrm-devel \ - libvdpau-devel \ - libX11-devel \ - libXext-devel \ - libXi-devel \ - libxkbcommon-x11-devel \ - libXScrnSaver-devel \ - libxcb-devel \ - libxcb-cursor-devel \ - libxcb-image-devel \ - libxcb-render-util-devel \ - libXinerama-devel 2>/dev/null || warn "Hardware acceleration libraries unavailable — FFmpeg will be software-decode only" - - # Optional: bundled into the AppImage by package-citron-linux.sh if present. - sudo dnf install -y gamemode 2>/dev/null || true - - # ── Linux audio output (ALSA + PulseAudio) — - sudo dnf install -y alsa-lib-devel pulseaudio-libs-devel \ - || error "ALSA/PulseAudio dev packages failed to install — Linux builds would have no audio output" } _setup_yum() { @@ -587,17 +427,9 @@ _setup_yum() { nasm yasm perl \ autoconf automake make \ clang lld patchelf \ - elfutils-libelf-devel openssl-devel libudev-devel zstd \ + 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." - - # Optional: bundled into the AppImage by package-citron-linux.sh if present. - # May require EPEL or an RPM Fusion-style repo on RHEL-based distros. - sudo yum install -y gamemode 2>/dev/null || true - - # ── Linux audio output (ALSA + PulseAudio) — - sudo yum install -y alsa-lib-devel pulseaudio-libs-devel \ - || error "ALSA/PulseAudio dev packages failed to install — Linux builds would have no audio output" } _setup_zypper() { @@ -608,23 +440,8 @@ _setup_zypper() { nasm yasm perl \ autoconf automake make \ glslang clang lld llvm patchelf \ - libelf-devel libopenssl-devel libudev-devel zstd \ + libelf-devel libopenssl-devel \ perf 2>/dev/null || true - - # Hardware video acceleration for bundled FFmpeg (VAAPI / VDPAU) - sudo zypper install -y --no-recommends \ - libva-devel \ - libdrm-devel \ - libvdpau-devel \ - libX11-devel \ - libXext-devel 2>/dev/null || warn "Hardware acceleration libraries unavailable — FFmpeg will be software-decode only" - - # Optional: bundled into the AppImage by package-citron-linux.sh if present. - sudo zypper install -y --no-recommends gamemode 2>/dev/null || true - - # ── Linux audio output (ALSA + PulseAudio) — - sudo zypper install -y --no-recommends alsa-devel libpulse-devel \ - || error "ALSA/PulseAudio dev packages failed to install — Linux builds would have no audio output" } _setup_emerge() { @@ -637,14 +454,7 @@ _setup_emerge() { dev-build/autoconf dev-build/automake \ media-libs/glslang \ dev-util/patchelf \ - dev-libs/elfutils dev-libs/openssl sys-apps/util-linux app-arch/zstd 2>/dev/null || true - - # Optional: bundled into the AppImage by package-citron-linux.sh if present. - sudo emerge --ask=n games-util/gamemode 2>/dev/null || true - - # ── Linux audio output (ALSA + PulseAudio) — - sudo emerge --ask=n media-libs/alsa-lib media-libs/libpulse \ - || error "ALSA/PulseAudio packages failed to install — Linux builds would have no audio output" + dev-libs/elfutils dev-libs/openssl 2>/dev/null || true } _install_llvm_clang() { @@ -715,11 +525,6 @@ _verify_tools() { else warn " aqt -> NOT FOUND (Qt download by cmake will fail)"; ok=0 fi - if ls /usr/lib*/libgamemode.so* &>/dev/null 2>&1 || ldconfig -p 2>/dev/null | grep -q libgamemode; then - success " gamemode -> available (will be bundled into AppImage)" - else - warn " gamemode -> not found (optional — AppImage will be built without it)" - fi [[ ${ok} -eq 1 ]] && success "All required tools available." \ || warn "Some tools missing — check output above." echo "" @@ -793,38 +598,28 @@ normalize_profraw_dirs() { } _collect_appimage_profiles() { - # Profiles generated by running the AppImage are written next to the - # AppImage file itself via AppDir/.env (LLVM_PROFILE_FILE=$(dirname - # "$APPIMAGE")/default-%p.profraw, set by package-citron-linux.sh). pkgforge's - # quick-sharun places the finished .AppImage in build_dir/AppImage/AppImage/, - # so that is where profraw lands. We collect it back to the main profile - # directory so it can be merged and used by the build script. - # - # Also check the legacy linuxdeploy location (build_dir/AppImage/) for - # compatibility with profiles collected before the pkgforge switch. + # 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 - for profile_src in "${BUILD_GENERATE}/AppImage/AppImage" "${BUILD_GENERATE}/AppImage"; do - if [[ -d "${profile_src}" ]]; then - local count; count="$(find "${profile_src}" -maxdepth 1 -name "*.profraw" 2>/dev/null | wc -l)" - if [[ "${count}" -gt 0 ]]; then - info "Collecting ${count} profile(s) from ${profile_src}..." - mv "${profile_src}"/*.profraw "${PROFILE_DIR}/" 2>/dev/null || true - fi + 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 - done + fi # CS-PGO profiles - for profile_src in "${BUILD_CSGENERATE}/AppImage/AppImage" "${BUILD_CSGENERATE}/AppImage"; do - if [[ -d "${profile_src}" ]]; then - local count; count="$(find "${profile_src}" -maxdepth 1 -name "*.profraw" 2>/dev/null | wc -l)" - if [[ "${count}" -gt 0 ]]; then - info "Collecting ${count} CS profile(s) from ${profile_src}..." - mkdir -p "${PROFILE_DIR}/cs" - mv "${profile_src}"/*.profraw "${PROFILE_DIR}/cs/" 2>/dev/null || true - fi + 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 - done + fi } _merge_profraw_to_profdata() { @@ -912,9 +707,8 @@ build_common_cmake_args() { "-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" - "-DCITRON_USE_QT_MULTIMEDIA=OFF" - "-DQT_NO_PRIVATE_MODULE_WARNING=ON" "-DENABLE_QT_TRANSLATION=ON" "-DUSE_DISCORD_PRESENCE=ON" "-DENABLE_WEB_SERVICE=ON" @@ -926,27 +720,10 @@ build_common_cmake_args() { "-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" ) - - # -mtls-dialect=gnu2 is x86_64-specific; skip for aarch64 - local host_arch; host_arch="$(uname -m)" - local use_tls_dialect=false - case "${_ARCH_ARG}" in - aarch64) use_tls_dialect=false ;; - auto) - [[ "${host_arch}" == "x86_64" ]] && use_tls_dialect=true || use_tls_dialect=false - ;; - *) - # Explicit x86_64 or v3 - [[ "${host_arch}" == "x86_64" ]] && use_tls_dialect=true || use_tls_dialect=false - ;; - esac - if [[ "${use_tls_dialect}" == "true" ]]; then - _CMAKE_ARGS+=("-DCMAKE_C_FLAGS=-mtls-dialect=gnu2") - _CMAKE_ARGS+=("-DCMAKE_CXX_FLAGS=-mtls-dialect=gnu2") - fi - [[ "${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. @@ -1030,19 +807,10 @@ _patch_binary_rpaths() { 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). - # - # Variable semantics from config.sh (set by CMake configure_file): - # CITRON_QT_PATH = QT_TARGET_PATH = Qt6 prefix (append /lib) - # CITRON_ICU_PATH = ICU_BINARY_DIR = ICU lib dir itself (do NOT append /lib) - # CITRON_XCB_PATH = XCB_BUILD_ROOT = XCB prefix (append /lib) - # - # ICU_BINARY_DIR is set as "${ICU_BUILD_DIR}/lib" in icu_build.cmake, so - # it already ends in /lib. Appending /lib again produces a wrong path like - # "icu-build/lib/lib" which does not exist on disk. + # 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}" + [[ -n "${icu_path}" ]] && rpath="${rpath}${rpath:+:}${icu_path}/lib" [[ -n "${xcb_path}" ]] && rpath="${rpath}${rpath:+:}${xcb_path}/lib" if [[ -z "${rpath}" ]]; then @@ -1051,29 +819,14 @@ _patch_binary_rpaths() { fi info "Patching RPATH for binaries in ${bin_dir}..." - # --force-rpath writes the old-style DT_RPATH tag instead of the default - # DT_RUNPATH. DT_RPATH is searched BEFORE LD_LIBRARY_PATH, whereas - # DT_RUNPATH is searched AFTER it. Without this, a dev's desktop session - # with LD_LIBRARY_PATH pointing at a distro/KDE Qt6 install can cause - # `ldd` (and the dynamic linker) to resolve libQt6Core.so.6 etc. to that - # OLDER system Qt instead of this CPM-built Qt — even though the binary - # was linked against the newer one — producing errors like: - # libQt6Core.so.6: version `Qt_6.9' not found - # DT_RPATH ensures our CPM-built Qt/ICU/XCB win regardless of the host's - # LD_LIBRARY_PATH. for bin in "citron" "citron-cmd" "citron-room"; do if [[ -f "${bin_dir}/${bin}" ]]; then - # Always re-apply --force-rpath to convert any DT_RUNPATH tag to - # DT_RPATH. Even when existing_rpath already contains our path, a - # DT_RUNPATH tag would still be searched AFTER LD_LIBRARY_PATH, - # letting a system Qt shadow the CPM-built one. --force-rpath - # ensures we always end up with DT_RPATH regardless of the linker's - # default tag choice. + # 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}" ]] || [[ "${existing_rpath}" == *"${rpath}"* ]]; then - patchelf --force-rpath --set-rpath "${rpath}${existing_rpath:+:${existing_rpath}}" "${bin_dir}/${bin}" - else - patchelf --force-rpath --set-rpath "${rpath}:${existing_rpath}" "${bin_dir}/${bin}" + 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 @@ -1126,130 +879,23 @@ build_appimage_stage() { local build_dir="$1" local stage_name="$2" local binary_override="${3:-}" - - if [[ "${NO_PACKAGE:-false}" == "true" ]]; then - info "Skipping AppImage packaging for ${stage_name} (--nopackage)." - return - fi - - header "Building AppImage for ${stage_name} (pkgforge)" - - # build_dir is the cmake build tree this binary was built in. For - # bolt/propeller, the caller passes the relocs-enabled build tree as - # build_dir, and the post-link-rewritten binary separately as - # binary_override (substituted into /usr/bin/citron below). - - # ── Stage install tree manually (not cmake --install) ───────────────────── - # pkgforge's quick-sharun expects a standard /usr layout (binary, desktop - # file, icon, and metadata under one prefix) so it can discover and bundle - # them. We deliberately do NOT run `cmake --install `: - # - # 1. It installs the ENTIRE configured project, including every - # CPM-fetched dependency (zlib, zstd, openal, opus, cubeb, ...) — - # gigabytes of headers/cmake-configs/pkgconfig files this AppImage - # never needs. - # 2. cubeb's cmake_install.cmake has a broken install(... include/cubeb) - # rule that resolves against citron's top-level source dir instead - # of cubeb's own source dir, and hard-fails `cmake --install` for - # the whole tree: - # CMake Error: file INSTALL cannot find ".../include/cubeb" - # - # citron's own install surface (src/citron/CMakeLists.txt + - # CMakeLists.txt) is exactly: the citron binary, plus four static files - # from dist/ (desktop entry, icon, mime type, appstream metadata). We - # replicate that small, known set directly — no sudo, no system changes, - # no CPM dependency install rules involved. - local install_root="${build_dir}/install-root" - rm -rf "${install_root}" - mkdir -p "${install_root}/usr/bin" \ - "${install_root}/usr/share/applications" \ - "${install_root}/usr/share/icons/hicolor/scalable/apps" \ - "${install_root}/usr/share/icons/hicolor/256x256/apps" \ - "${install_root}/usr/share/mime/packages" \ - "${install_root}/usr/share/metainfo" - - info "Staging install tree for ${stage_name} (no cmake --install, no sudo)..." - info " Staging root: ${install_root}" - - [[ -f "${build_dir}/bin/citron" ]] \ - || error "citron binary not found at ${build_dir}/bin/citron" - cp "${build_dir}/bin/citron" "${install_root}/usr/bin/citron" - - cp "${SCRIPT_DIR}/dist/org.citron_emu.citron.desktop" \ - "${install_root}/usr/share/applications/" - cp "${SCRIPT_DIR}/dist/citron.svg" \ - "${install_root}/usr/share/icons/hicolor/scalable/apps/org.citron_emu.citron.svg" - if [ -f "${SCRIPT_DIR}/dist/org.citron_emu.citron.png" ]; then - cp "${SCRIPT_DIR}/dist/org.citron_emu.citron.png" \ - "${install_root}/usr/share/icons/hicolor/256x256/apps/" - fi - cp "${SCRIPT_DIR}/dist/org.citron_emu.citron.xml" \ - "${install_root}/usr/share/mime/packages/" 2>/dev/null || true - cp "${SCRIPT_DIR}/dist/org.citron_emu.citron.metainfo.xml" \ - "${install_root}/usr/share/metainfo/" 2>/dev/null || true - - # Qt translations: citron's own cmake does not install these. Pull them - # directly from the aqt-downloaded Qt under CPM_SOURCE_CACHE/qt-bin - # (see CMakeModules/qt_download.cmake) if present. - local qt_translations - qt_translations="$(find "${CPM_SOURCE_CACHE}/qt-bin" -maxdepth 4 -type d \ - -name 'translations' 2>/dev/null | head -1)" - if [[ -n "${qt_translations}" ]]; then - mkdir -p "${install_root}/usr/share/qt6" - cp -r "${qt_translations}" "${install_root}/usr/share/qt6/translations" - fi + header "Building AppImage for ${stage_name}" if [[ -n "${binary_override}" ]]; then - # The bolt/propeller-rewritten binary replaces the cmake-installed one. - info "Substituting BOLT/Propeller-optimized binary..." - cp "${binary_override}" "${install_root}/usr/bin/citron" + # 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 - # ── Package via pkgforge (quick-sharun) ────────────────────────────────── - local pkg_work="${build_dir}/AppImage" - rm -rf "${pkg_work}" - mkdir -p "${pkg_work}/user" - cd "${pkg_work}" + # Run the AppImage build script + bash "${SCRIPT_DIR}/AppImageBuilder/build-v2.sh" "${build_dir}" - local version="${APP_VERSION:-}" - if [[ -z "${version}" ]]; then - version="$(git -C "${SCRIPT_DIR}" rev-parse --short HEAD 2>/dev/null || echo "${stage_name}")" - fi - local arch_suffix="${ARCH_SUFFIX:-"-${stage_name}"}" + # Create the output directory and move the AppImage + mkdir -p "${build_dir}/AppImage/user" + mv "${build_dir}/citron-x86_64.AppImage" "${build_dir}/AppImage/" - # Extract CITRON_QT_PATH from config.sh so package-citron-linux.sh can - # derive QT_LOCATION and point quick-sharun at CPM's Qt plugins rather - # than the system Qt's plugins (system plugins depend on the system - # libQt6Core.so.6 which may be older than the CPM-built Qt 6.9.3 citron - # was compiled against, causing "libQt6Core.so.6: version 'Qt_6.9' not - # found" at AppImage runtime). - local config_file="${build_dir}/AppImageBuilder/config.sh" - local qt_path="" - if [[ -f "${config_file}" ]]; then - qt_path="$(grep -oP '(?<=export CITRON_QT_PATH=")[^"]+' "${config_file}" || true)" - fi - if [[ -z "${qt_path}" ]]; then - error "CITRON_QT_PATH not found in ${config_file} — cannot package AppImage with correct Qt plugins. Ensure the build completed successfully and config.sh was written." - fi - - APP_VERSION="${version}" \ - ARCH_SUFFIX="${arch_suffix}" \ - DEVEL="${DEVEL:-false}" \ - OUTPATH="${pkg_work}" \ - DESTDIR="${install_root}" \ - CITRON_QT_PATH="${qt_path}" \ - bash "${SCRIPT_DIR}/AppImageBuilder/package-citron-linux.sh" \ - || error "package-citron-linux.sh failed for ${stage_name}" - - cd "${build_dir}" - - local appimage - appimage="$(find "${pkg_work}" -maxdepth 1 -name '*.AppImage' | head -1)" - if [[ -z "${appimage}" ]]; then - error "package-citron-linux.sh did not produce an AppImage in ${pkg_work}" - fi - - success "AppImage for ${stage_name} placed in ${appimage}" + success "AppImage for ${stage_name} placed in ${build_dir}/AppImage/citron-x86_64.AppImage" } # ============================================================================= @@ -1735,8 +1381,6 @@ while [[ $# -gt 0 ]]; do --relwithdebinfo) BUILD_TYPE="RelWithDebInfo"; shift ;; --clang-version) CLANG_VERSION="$2"; _set_clang_tools; shift 2 ;; - --nopackage|--no-package) - NO_PACKAGE=true; shift ;; --help|-h) sed -n '/^# build-citron-linux/,/^# ===/p' "$0" | head -130 exit 0 ;; diff --git a/build-for-mingw.bat b/build-for-mingw.bat new file mode 100644 index 0000000000..abd9abacc6 --- /dev/null +++ b/build-for-mingw.bat @@ -0,0 +1,86 @@ +@echo off +REM SPDX-FileCopyrightText: 2026 citron Emulator Project +REM SPDX-License-Identifier: GPL-3.0-or-later +REM +REM One-click MinGW build launcher for Citron Neo. +REM Finds your MSYS2 installation and runs build-for-mingw.sh in UCRT64 mode. + +setlocal enabledelayedexpansion + +REM ---------- locate MSYS2 ---------- +set "MSYS2_PATH=" + +REM Check common install locations +for %%P in ( + "C:\msys64" + "C:\msys2" + "%USERPROFILE%\msys64" + "%USERPROFILE%\msys2" + "D:\msys64" + "D:\msys2" +) do ( + if exist "%%~P\usr\bin\bash.exe" ( + set "MSYS2_PATH=%%~P" + goto :found_msys2 + ) +) + +REM Try the PATH +where bash.exe >nul 2>&1 +if %ERRORLEVEL% equ 0 ( + for /f "delims=" %%I in ('where bash.exe') do ( + set "BASH_LOC=%%~dpI" + if exist "!BASH_LOC!..\..\..\usr\bin\bash.exe" ( + for %%J in ("!BASH_LOC!..\..\..") do set "MSYS2_PATH=%%~fJ" + goto :found_msys2 + ) + ) +) + +echo. +echo ERROR: Could not find an MSYS2 installation. +echo. +echo Please install MSYS2 from https://www.msys2.org/ and try again, +echo or set MSYS2_PATH before running this script: +echo. +echo set MSYS2_PATH=C:\msys64 +echo build-for-mingw.bat +echo. +pause +exit /b 1 + +:found_msys2 +echo Found MSYS2 at: %MSYS2_PATH% + +REM ---------- resolve script path ---------- +set "SCRIPT_DIR=%~dp0" +REM Remove trailing backslash +if "%SCRIPT_DIR:~-1%"=="\" set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%" + +REM Convert Windows path to MSYS2 path (C:\foo\bar -> /c/foo/bar) +set "MSYS_SOURCE=%SCRIPT_DIR:\=/%" +set "MSYS_SOURCE=/%MSYS_SOURCE:~0,1%%MSYS_SOURCE:~2%" +REM Lowercase the drive letter +for %%a in (a b c d e f g h i j k l m n o p q r s t u v w x y z) do ( + set "MSYS_SOURCE=!MSYS_SOURCE:/%%a/=/%%a/!" +) + +REM ---------- run the build ---------- +echo. +echo Launching MSYS2 UCRT64 build ... +echo. + +"%MSYS2_PATH%\usr\bin\env.exe" MSYSTEM=UCRT64 ^ + "%MSYS2_PATH%\usr\bin\bash.exe" -lc ^ + "cd '%MSYS_SOURCE%' && bash build-for-mingw.sh %*" + +if %ERRORLEVEL% neq 0 ( + echo. + echo Build failed with error code %ERRORLEVEL%. + pause + exit /b %ERRORLEVEL% +) + +echo. +echo Build succeeded! Executables are in build-mingw\bin\ +pause diff --git a/build-for-mingw.sh b/build-for-mingw.sh new file mode 100644 index 0000000000..28fd915653 --- /dev/null +++ b/build-for-mingw.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2026 citron Emulator Project +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Automated MinGW (MSYS2 UCRT64) build script for Citron Neo. +# Run from the MSYS2 UCRT64 shell, or use build-for-mingw.bat to launch it. + +set -euo pipefail + +# ---------- configuration ---------- +BUILD_DIR="${1:-build-mingw}" +BUILD_TYPE="${2:-Release}" +JOBS="${3:-$(nproc)}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SOURCE_DIR="$SCRIPT_DIR" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +info() { echo -e "${CYAN}[INFO]${NC} $*"; } +ok() { echo -e "${GREEN}[ OK ]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +fail() { echo -e "${RED}[FAIL]${NC} $*"; exit 1; } + +# ---------- environment check ---------- +if [[ -z "${MSYSTEM:-}" ]]; then + fail "Not running inside an MSYS2 shell. Use the UCRT64 terminal or build-for-mingw.bat." +fi +if [[ "$MSYSTEM" != "UCRT64" ]]; then + warn "MSYSTEM is '$MSYSTEM', expected 'UCRT64'. Builds may use the wrong toolchain." +fi + +command -v pacman >/dev/null 2>&1 || fail "pacman not found — is this really MSYS2?" + +# ---------- dependency installation ---------- +info "Installing / updating build tools and dependencies via pacman …" + +PACKAGES=( + # build tools + mingw-w64-ucrt-x86_64-cmake + mingw-w64-ucrt-x86_64-ninja + mingw-w64-ucrt-x86_64-toolchain + + # required libraries + mingw-w64-ucrt-x86_64-boost + mingw-w64-ucrt-x86_64-fmt + mingw-w64-ucrt-x86_64-nlohmann-json + mingw-w64-ucrt-x86_64-opus + mingw-w64-ucrt-x86_64-SDL2 + mingw-w64-ucrt-x86_64-qt6-base + mingw-w64-ucrt-x86_64-qt6-multimedia + mingw-w64-ucrt-x86_64-qt6-svg + mingw-w64-ucrt-x86_64-qt6-tools + mingw-w64-ucrt-x86_64-ffmpeg + mingw-w64-ucrt-x86_64-openal + mingw-w64-ucrt-x86_64-vulkan-headers + mingw-w64-ucrt-x86_64-vulkan-utility-libraries + mingw-w64-ucrt-x86_64-vulkan-memory-allocator + mingw-w64-ucrt-x86_64-libusb + mingw-w64-ucrt-x86_64-enet + mingw-w64-ucrt-x86_64-stb +) + +pacman -S --needed --noconfirm "${PACKAGES[@]}" || fail "pacman install failed" +ok "All dependencies installed." + +# ---------- configure ---------- +info "Configuring CMake (${BUILD_TYPE}) in ${BUILD_DIR} …" +mkdir -p "${SOURCE_DIR}/${BUILD_DIR}" +cd "${SOURCE_DIR}/${BUILD_DIR}" + +cmake "$SOURCE_DIR" -G Ninja \ + -DCMAKE_BUILD_TYPE="$BUILD_TYPE" \ + -DCITRON_USE_BUNDLED_VCPKG=OFF \ + -DCITRON_USE_BUNDLED_SDL2=OFF \ + -DCITRON_USE_BUNDLED_QT=OFF \ + -DCITRON_USE_BUNDLED_FFMPEG=OFF \ + -DCITRON_USE_EXTERNAL_VULKAN_HEADERS=OFF \ + -DCITRON_USE_EXTERNAL_VULKAN_UTILITY_LIBRARIES=OFF \ + -DENABLE_QT_TRANSLATION=OFF \ + -DUSE_DISCORD_PRESENCE=OFF \ + -DCITRON_TESTS=OFF \ + -DENABLE_WEB_SERVICE=OFF \ + -DCITRON_USE_FASTER_LD=OFF \ + || fail "CMake configuration failed" + +ok "CMake configuration succeeded." + +# ---------- build ---------- +info "Building with ${JOBS} parallel jobs …" +ninja -j"$JOBS" || fail "Build failed" + +ok "Build complete!" +echo "" +info "Executables are in: ${SOURCE_DIR}/${BUILD_DIR}/bin/" +ls -lh "${SOURCE_DIR}/${BUILD_DIR}/bin/"*.exe 2>/dev/null || true +echo "" +ok "Done. You can run citron from: ${BUILD_DIR}/bin/citron.exe" diff --git a/dist/org.citron_emu.citron.png b/dist/org.citron_emu.citron.png deleted file mode 100644 index e844500ec0..0000000000 Binary files a/dist/org.citron_emu.citron.png and /dev/null differ diff --git a/externals/libusb/CMakeLists.txt b/externals/libusb/CMakeLists.txt index 982241d67c..1d50c9f8c8 100644 --- a/externals/libusb/CMakeLists.txt +++ b/externals/libusb/CMakeLists.txt @@ -20,16 +20,7 @@ if (MINGW OR (${CMAKE_SYSTEM_NAME} MATCHES "Linux") OR APPLE) endif() set(LIBUSB_PREFIX "${CMAKE_CURRENT_BINARY_DIR}/libusb") - # Source comes exclusively from CPM (libusb_src declared in dependencies.cmake). - # LIBUSB_CPM_SOURCE_DIR is the CACHE INTERNAL bridge set there; libusb_src_SOURCE_DIR - # is CPM's own auto-set and is always present even on a cache hit where _ADDED is false. - if (DEFINED LIBUSB_CPM_SOURCE_DIR) - set(LIBUSB_SRC_DIR "${LIBUSB_CPM_SOURCE_DIR}") - elseif (DEFINED libusb_src_SOURCE_DIR) - set(LIBUSB_SRC_DIR "${libusb_src_SOURCE_DIR}") - else() - set(LIBUSB_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libusb") - endif() + set(LIBUSB_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libusb") # Workarounds for MSYS/MinGW if (MSYS) diff --git a/fix-pgo-dll.ps1 b/fix-pgo-dll.ps1 new file mode 100644 index 0000000000..2b73f1c33b --- /dev/null +++ b/fix-pgo-dll.ps1 @@ -0,0 +1,174 @@ +# SPDX-FileCopyrightText: 2025 citron Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later + +# Fix PGO DLL Script for Citron (Windows) +# This script helps locate and copy the required pgort140.dll for MSVC PGO builds + +param( + [Parameter()] + [string]$OutputDir = "build\bin\Release", + + [Parameter()] + [switch]$Help +) + +function Show-Usage { + Write-Host @" +Usage: .\fix-pgo-dll.ps1 [OPTIONS] + +This script helps fix the "pgort140.DLL was not found" error by locating +and copying the required PGO runtime DLL to your build output directory. + +Options: + -OutputDir PATH Target directory to copy DLL (default: build\bin\Release) + -Help Show this help message + +Example: + .\fix-pgo-dll.ps1 + .\fix-pgo-dll.ps1 -OutputDir "C:\MyBuild\bin" +"@ +} + +function Write-Header { + param([string]$Message) + Write-Host "`n=================================================================" -ForegroundColor Cyan + Write-Host $Message -ForegroundColor Cyan + Write-Host "=================================================================`n" -ForegroundColor Cyan +} + +function Write-Info { + param([string]$Message) + Write-Host "[INFO] $Message" -ForegroundColor Green +} + +function Write-Warning { + param([string]$Message) + Write-Host "[WARNING] $Message" -ForegroundColor Yellow +} + +function Write-Error-Custom { + param([string]$Message) + Write-Host "[ERROR] $Message" -ForegroundColor Red +} + +if ($Help) { + Show-Usage + exit 0 +} + +Write-Header "PGO DLL Fixer for Citron" + +# Find Visual Studio installation +$VSInstallPath = $null +$VSWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + +if (Test-Path $VSWhere) { + Write-Info "Searching for Visual Studio installations..." + $VSInstallPath = & $VSWhere -latest -property installationPath + if ($VSInstallPath) { + Write-Info "Found Visual Studio at: $VSInstallPath" + } +} else { + Write-Warning "vswhere.exe not found. Trying common installation paths..." +} + +# Common Visual Studio installation paths +$CommonPaths = @( + "${env:ProgramFiles}\Microsoft Visual Studio\2022\Community", + "${env:ProgramFiles}\Microsoft Visual Studio\2022\Professional", + "${env:ProgramFiles}\Microsoft Visual Studio\2022\Enterprise", + "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2019\Community", + "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2019\Professional", + "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2019\Enterprise" +) + +if (-not $VSInstallPath) { + foreach ($path in $CommonPaths) { + if (Test-Path $path) { + $VSInstallPath = $path + Write-Info "Found Visual Studio at: $VSInstallPath" + break + } + } +} + +if (-not $VSInstallPath) { + Write-Error-Custom "Visual Studio installation not found!" + Write-Host "Please ensure Visual Studio 2019 or 2022 is installed with C++ support." + Write-Host "You can download it from: https://visualstudio.microsoft.com/downloads/" + exit 1 +} + +# Search for pgort140.dll +Write-Info "Searching for pgort140.dll..." + +$DllPaths = @( + "$VSInstallPath\VC\Redist\MSVC\*\x64\Microsoft.VC*.CRT\pgort140.dll", + "$VSInstallPath\VC\Redist\MSVC\*\x86\Microsoft.VC*.CRT\pgort140.dll", + "$VSInstallPath\VC\Tools\MSVC\*\bin\Hostx64\x64\pgort140.dll", + "$VSInstallPath\VC\Tools\MSVC\*\bin\Hostx64\x86\pgort140.dll", + "$VSInstallPath\VC\Tools\MSVC\*\bin\Hostx86\x64\pgort140.dll", + "$VSInstallPath\VC\Tools\MSVC\*\bin\Hostx86\x86\pgort140.dll" +) + +$FoundDll = $null +foreach ($pattern in $DllPaths) { + $matches = Get-ChildItem -Path $pattern -ErrorAction SilentlyContinue + if ($matches) { + $FoundDll = $matches[0].FullName + Write-Info "Found pgort140.dll at: $FoundDll" + break + } +} + +if (-not $FoundDll) { + Write-Error-Custom "pgort140.dll not found in Visual Studio installation!" + Write-Host "This usually means:" + Write-Host "1. Visual Studio was installed without PGO support" + Write-Host "2. You need to install the 'MSVC v143 - VS 2022 C++ x64/x86 build tools' component" + Write-Host "3. Try repairing your Visual Studio installation" + Write-Host "" + Write-Host "To fix this:" + Write-Host "1. Open Visual Studio Installer" + Write-Host "2. Click 'Modify' on your Visual Studio installation" + Write-Host "3. Go to 'Individual components' tab" + Write-Host "4. Search for 'PGO' and ensure it's checked" + Write-Host "5. Click 'Modify' to install the component" + exit 1 +} + +# Create output directory if it doesn't exist +if (-not (Test-Path $OutputDir)) { + Write-Info "Creating output directory: $OutputDir" + New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null +} + +# Copy the DLL +try { + Write-Info "Copying pgort140.dll to: $OutputDir" + Copy-Item -Path $FoundDll -Destination $OutputDir -Force + Write-Info "Successfully copied pgort140.dll!" + + # Verify the copy + $CopiedDll = Join-Path $OutputDir "pgort140.dll" + if (Test-Path $CopiedDll) { + Write-Info "Verification: pgort140.dll is now available in $OutputDir" + Write-Host "" + Write-Host "You can now run your PGO instrumented Citron build!" + } else { + Write-Error-Custom "Failed to copy pgort140.dll" + exit 1 + } +} catch { + Write-Error-Custom "Error copying pgort140.dll: $($_.Exception.Message)" + exit 1 +} + +Write-Header "PGO DLL Fix Complete!" +Write-Info "The pgort140.dll has been copied to your build output directory." +Write-Info "Your PGO instrumented Citron build should now run without the DLL error." +Write-Host "" +Write-Info "Next steps:" +Write-Host "1. Run your PGO instrumented build" +Write-Host "2. Play games to collect profile data" +Write-Host "3. Rebuild with PGO USE stage for optimization" diff --git a/pgo-build.ps1 b/pgo-build.ps1 new file mode 100644 index 0000000000..081afac8e1 --- /dev/null +++ b/pgo-build.ps1 @@ -0,0 +1,344 @@ +# SPDX-FileCopyrightText: 2025 citron Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later + +# PGO Build Script for Citron (Windows/PowerShell) +# This script automates the Profile-Guided Optimization build process + +param( + [Parameter(Position=0, Mandatory=$true)] + [ValidateSet('generate', 'use', 'clean', 'merge', 'summary')] + [string]$Stage, + + [Parameter()] + [int]$Jobs = 0, + + [Parameter()] + [switch]$EnableLTO, + + [Parameter()] + [switch]$Help +) + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$BuildDir = Join-Path $ScriptDir "build" +$PgoProfilesDir = Join-Path $BuildDir "pgo-profiles" +$BackupProfilesDir = Join-Path $ScriptDir "pgo-profiles-backup" + +function Show-Usage { + Write-Host @" +Usage: .\pgo-build.ps1 [STAGE] [OPTIONS] + +STAGE can be: + generate - Build with PGO instrumentation (Stage 1) + use - Build using PGO profile data (Stage 2) + clean - Clean build directory but preserve profiles + merge - Merge PGC files into PGD without rebuilding + summary - Show profile coverage statistics + +Example workflow: + .\pgo-build.ps1 generate # Build instrumented version + # Run citron.exe, play 2-3 games for 5-10 min each, exit cleanly + .\pgo-build.ps1 merge # Merge collected profiles + .\pgo-build.ps1 summary # Check coverage + .\pgo-build.ps1 use # Build optimized version + +Options: + -Jobs N Number of parallel jobs (default: auto-detect) + -EnableLTO Enable Link-Time Optimization + -Help Show this help message +"@ +} + +function Write-Header { + param([string]$Message) + Write-Host "`n=================================================================" -ForegroundColor Cyan + Write-Host $Message -ForegroundColor Cyan + Write-Host "=================================================================`n" -ForegroundColor Cyan +} + +function Write-Info { + param([string]$Message) + Write-Host "[INFO] $Message" -ForegroundColor Green +} + +function Write-Warning { + param([string]$Message) + Write-Host "[WARNING] $Message" -ForegroundColor Yellow +} + +function Write-Error-Custom { + param([string]$Message) + Write-Host "[ERROR] $Message" -ForegroundColor Red +} + +function Find-Pgomgr { + $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + if (Test-Path $vswhere) { + $vsPath = & $vswhere -latest -property installationPath + $pgomgr = Get-ChildItem -Path "$vsPath\VC\Tools\MSVC" -Recurse -Filter "pgomgr.exe" -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -match "Hostx64\\x64" } | + Select-Object -First 1 + if ($pgomgr) { return $pgomgr.FullName } + } + $found = Get-Command pgomgr.exe -ErrorAction SilentlyContinue + if ($found) { return $found.Source } + return $null +} + +function Clean-StaleProfiles { + if (-not (Test-Path $PgoProfilesDir)) { return } + + $pgcFiles = Get-ChildItem -Path $PgoProfilesDir -Filter "*.pgc" -ErrorAction SilentlyContinue + if ($pgcFiles -and $pgcFiles.Count -gt 0) { + Write-Info "Removing $($pgcFiles.Count) stale PGC file(s)..." + $pgcFiles | Remove-Item -Force + } +} + +function Merge-Profiles { + $pgomgr = Find-Pgomgr + if (-not $pgomgr) { + Write-Warning "pgomgr.exe not found. Cannot merge profiles." + Write-Info "Ensure Visual Studio Build Tools are installed and on PATH." + return $false + } + + $pgdFiles = Get-ChildItem -Path $PgoProfilesDir -Filter "*.pgd" -ErrorAction SilentlyContinue + if (-not $pgdFiles -or $pgdFiles.Count -eq 0) { + Write-Warning "No PGD files found in $PgoProfilesDir" + return $false + } + + $pgcFiles = Get-ChildItem -Path $PgoProfilesDir -Filter "*.pgc" -ErrorAction SilentlyContinue + if (-not $pgcFiles -or $pgcFiles.Count -eq 0) { + Write-Info "No PGC files to merge." + return $true + } + + foreach ($pgd in $pgdFiles) { + Write-Info "Merging PGC files into $($pgd.Name)..." + & $pgomgr /merge $pgd.FullName + if ($LASTEXITCODE -ne 0) { + Write-Warning "pgomgr /merge returned exit code $LASTEXITCODE for $($pgd.Name)" + } + } + + # Clean PGC files after successful merge + Write-Info "Cleaning merged PGC files..." + Get-ChildItem -Path $PgoProfilesDir -Filter "*.pgc" -ErrorAction SilentlyContinue | Remove-Item -Force + + return $true +} + +function Show-ProfileSummary { + $pgomgr = Find-Pgomgr + if (-not $pgomgr) { + Write-Warning "pgomgr.exe not found. Cannot show summary." + return + } + + $pgdFiles = Get-ChildItem -Path $PgoProfilesDir -Filter "*.pgd" -ErrorAction SilentlyContinue + if (-not $pgdFiles -or $pgdFiles.Count -eq 0) { + Write-Warning "No PGD files found in $PgoProfilesDir" + return + } + + foreach ($pgd in $pgdFiles) { + Write-Header "Profile Summary: $($pgd.Name)" + & $pgomgr /summary $pgd.FullName + Write-Host "" + } +} + +if ($Help) { + Show-Usage + exit 0 +} + +# Auto-detect number of processors +if ($Jobs -eq 0) { + $Jobs = $env:NUMBER_OF_PROCESSORS + if (-not $Jobs) { $Jobs = 4 } +} + +$LtoFlag = if ($EnableLTO) { "ON" } else { "OFF" } + +# --- Clean stage --- +if ($Stage -eq "clean") { + Write-Header "Cleaning Build Directory" + + if (Test-Path $PgoProfilesDir) { + Write-Info "Backing up PGO profiles..." + New-Item -ItemType Directory -Force -Path $BackupProfilesDir | Out-Null + Copy-Item -Path "$PgoProfilesDir\*" -Destination $BackupProfilesDir -Recurse -Force -ErrorAction SilentlyContinue + } + + if (Test-Path $BuildDir) { + Write-Info "Removing build directory..." + Remove-Item -Path $BuildDir -Recurse -Force + } + + if (Test-Path $BackupProfilesDir) { + Write-Info "Restoring PGO profiles..." + New-Item -ItemType Directory -Force -Path $PgoProfilesDir | Out-Null + Move-Item -Path "$BackupProfilesDir\*" -Destination $PgoProfilesDir -Force -ErrorAction SilentlyContinue + Remove-Item -Path $BackupProfilesDir -Recurse -Force + } + + Write-Info "Clean complete!" + exit 0 +} + +# --- Merge stage --- +if ($Stage -eq "merge") { + Write-Header "Merging PGO Profiles" + + if (-not (Test-Path $PgoProfilesDir)) { + Write-Error-Custom "Profile directory not found: $PgoProfilesDir" + exit 1 + } + + $result = Merge-Profiles + if ($result) { + Write-Info "Merge complete! Run '.\pgo-build.ps1 summary' to check coverage." + } else { + Write-Error-Custom "Merge failed." + exit 1 + } + exit 0 +} + +# --- Summary stage --- +if ($Stage -eq "summary") { + Write-Header "PGO Profile Summary" + + if (-not (Test-Path $PgoProfilesDir)) { + Write-Error-Custom "Profile directory not found: $PgoProfilesDir" + exit 1 + } + + Show-ProfileSummary + exit 0 +} + +# --- Generate stage --- +if ($Stage -eq "generate") { + Write-Header "PGO Stage 1: Generate Profile Data" + + # Clean stale PGC files from any previous run + if (Test-Path $PgoProfilesDir) { + Clean-StaleProfiles + } + + # Create build directory + New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null + Set-Location $BuildDir + + # Configure + Write-Info "Configuring CMake..." + cmake .. ` + -DCITRON_ENABLE_PGO_GENERATE=ON ` + -DCITRON_ENABLE_LTO=$LtoFlag ` + -DCMAKE_BUILD_TYPE=Release + + if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "CMake configuration failed" + exit 1 + } + + # Build + Write-Info "Building instrumented Citron (this may take a while)..." + cmake --build . --config Release -j $Jobs + + if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "Build failed" + exit 1 + } + + Write-Header "Build Complete!" + Write-Host "" + Write-Host " Training guide for best PGO results:" -ForegroundColor Yellow + Write-Host "" + Write-Host " 1. Run: .\bin\Release\citron.exe" + Write-Host " 2. Launch a game and play PAST initial loading" + Write-Host " (first-time shader compilation is a critical hot path)" + Write-Host " 3. Play for at least 5-10 minutes per game" + Write-Host " 4. Test 2-3 different games for broader code coverage" + Write-Host " 5. Navigate menus, settings, and game list to profile the UI" + Write-Host " 6. Exit citron cleanly (File -> Exit or Ctrl+Q)" + Write-Host "" + Write-Host " After each session, you can run:" -ForegroundColor Yellow + Write-Host " .\pgo-build.ps1 merge # Consolidate collected profiles" + Write-Host " .\pgo-build.ps1 summary # Check profile coverage" + Write-Host "" + Write-Host " When satisfied with coverage, build the optimized binary:" -ForegroundColor Yellow + Write-Host " .\pgo-build.ps1 use" + Write-Host "" + + Set-Location $ScriptDir +} + +# --- Use stage --- +if ($Stage -eq "use") { + Write-Header "PGO Stage 2: Build Optimized Binary" + + # Check if profile data exists + if (-not (Test-Path $PgoProfilesDir) -or -not (Get-ChildItem $PgoProfilesDir -ErrorAction SilentlyContinue)) { + Write-Error-Custom "No profile data found in $PgoProfilesDir" + Write-Info "Please run the generate stage first and collect profile data" + exit 1 + } + + # Merge any outstanding PGC files before building + Write-Info "Merging any outstanding PGC files..." + Merge-Profiles | Out-Null + + # Backup profiles if build directory exists + if (Test-Path $BuildDir) { + Write-Info "Backing up PGO profiles..." + New-Item -ItemType Directory -Force -Path $BackupProfilesDir | Out-Null + Copy-Item -Path "$PgoProfilesDir\*" -Destination $BackupProfilesDir -Recurse -Force + Remove-Item -Path $BuildDir -Recurse -Force + } + + # Create build directory and restore profiles + New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null + if (Test-Path $BackupProfilesDir) { + New-Item -ItemType Directory -Force -Path $PgoProfilesDir | Out-Null + Move-Item -Path "$BackupProfilesDir\*" -Destination $PgoProfilesDir -Force + Remove-Item -Path $BackupProfilesDir -Recurse -Force + } + + Set-Location $BuildDir + + # Configure + Write-Info "Configuring CMake..." + cmake .. ` + -DCITRON_ENABLE_PGO_USE=ON ` + -DCITRON_ENABLE_LTO=$LtoFlag ` + -DCMAKE_BUILD_TYPE=Release + + if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "CMake configuration failed" + Set-Location $ScriptDir + exit 1 + } + + # Build + Write-Info "Building optimized Citron (this may take a while)..." + cmake --build . --config Release -j $Jobs + + if ($LASTEXITCODE -ne 0) { + Write-Error-Custom "Build failed" + Set-Location $ScriptDir + exit 1 + } + + Write-Header "Build Complete!" + Write-Info "Your optimized Citron binary is ready!" + Write-Info "Location: $BuildDir\bin\Release\citron.exe" + Write-Host "" + Write-Info "This build is optimized for your specific usage patterns." + + Set-Location $ScriptDir +} diff --git a/pgo-build.sh b/pgo-build.sh new file mode 100644 index 0000000000..a60a0c8bf6 --- /dev/null +++ b/pgo-build.sh @@ -0,0 +1,328 @@ +#!/bin/bash +# SPDX-FileCopyrightText: 2025 citron Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later + +# PGO Build Script for Citron (Linux/macOS) +# This script automates the Profile-Guided Optimization build process + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BUILD_DIR="${SCRIPT_DIR}/build" +PGO_PROFILES_DIR="${BUILD_DIR}/pgo-profiles" +BACKUP_PROFILES_DIR="${SCRIPT_DIR}/pgo-profiles-backup" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +print_header() { + echo -e "${BLUE}=================================================================${NC}" + echo -e "${BLUE}$1${NC}" + echo -e "${BLUE}=================================================================${NC}" +} + +print_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +show_usage() { + echo "Usage: $0 [STAGE] [OPTIONS]" + echo "" + echo "STAGE can be:" + echo " generate - Build with PGO instrumentation (Stage 1)" + echo " use - Build using PGO profile data (Stage 2)" + echo " clean - Clean build directory but preserve profiles" + echo " merge - Merge profile data (Clang: .profraw -> .profdata)" + echo " summary - Show profile file statistics" + echo "" + echo "Example workflow:" + echo " $0 generate # Build instrumented version" + echo " # Run citron, play 2-3 games for 5-10 min each, exit cleanly" + echo " $0 merge # Merge collected profiles" + echo " $0 summary # Check coverage" + echo " $0 use # Build optimized version" + echo "" + echo "Options:" + echo " -j N Number of parallel jobs (default: auto-detect)" + echo " -lto Enable Link-Time Optimization" + echo " -h Show this help message" +} + +clean_stale_profiles() { + if [ ! -d "$PGO_PROFILES_DIR" ]; then + return + fi + + local gcda_count + gcda_count=$(find "$PGO_PROFILES_DIR" -name "*.gcda" 2>/dev/null | wc -l) + if [ "$gcda_count" -gt 0 ]; then + print_info "Removing $gcda_count stale .gcda file(s)..." + find "$PGO_PROFILES_DIR" -name "*.gcda" -delete + fi + + local profraw_count + profraw_count=$(find "$PGO_PROFILES_DIR" -name "*.profraw" 2>/dev/null | wc -l) + if [ "$profraw_count" -gt 0 ]; then + print_info "Removing $profraw_count stale .profraw file(s)..." + find "$PGO_PROFILES_DIR" -name "*.profraw" -delete + fi +} + +merge_clang_profiles() { + if ! command -v llvm-profdata &>/dev/null; then + print_warning "llvm-profdata not found. Cannot merge Clang profiles." + return 1 + fi + + local profraw_files + profraw_files=$(find "$PGO_PROFILES_DIR" -name "*.profraw" 2>/dev/null) + if [ -z "$profraw_files" ]; then + print_info "No .profraw files to merge." + return 0 + fi + + local count + count=$(echo "$profraw_files" | wc -l) + print_info "Merging $count .profraw file(s)..." + + llvm-profdata merge \ + -output="${PGO_PROFILES_DIR}/default.profdata" \ + ${profraw_files} + + print_info "Merged into ${PGO_PROFILES_DIR}/default.profdata" + + # Clean raw files after merge + print_info "Cleaning merged .profraw files..." + find "$PGO_PROFILES_DIR" -name "*.profraw" -delete + + return 0 +} + +show_profile_summary() { + if [ ! -d "$PGO_PROFILES_DIR" ]; then + print_warning "Profile directory not found: $PGO_PROFILES_DIR" + return + fi + + print_header "Profile Summary" + + local profdata="${PGO_PROFILES_DIR}/default.profdata" + if [ -f "$profdata" ]; then + local size + size=$(du -h "$profdata" | cut -f1) + print_info "Clang profile: $profdata ($size)" + if command -v llvm-profdata &>/dev/null; then + llvm-profdata show --counts --all-functions "$profdata" 2>/dev/null | tail -5 + fi + fi + + local gcda_count + gcda_count=$(find "$PGO_PROFILES_DIR" -name "*.gcda" 2>/dev/null | wc -l) + if [ "$gcda_count" -gt 0 ]; then + print_info "GCC profile files: $gcda_count .gcda files" + local total_size + total_size=$(find "$PGO_PROFILES_DIR" -name "*.gcda" -exec du -ch {} + 2>/dev/null | tail -1 | cut -f1) + print_info "Total .gcda size: $total_size" + fi + + local profraw_count + profraw_count=$(find "$PGO_PROFILES_DIR" -name "*.profraw" 2>/dev/null | wc -l) + if [ "$profraw_count" -gt 0 ]; then + print_warning "$profraw_count unmerged .profraw file(s) found. Run '$0 merge' first." + fi + + if [ "$gcda_count" -eq 0 ] && [ ! -f "$profdata" ] && [ "$profraw_count" -eq 0 ]; then + print_warning "No profile data found in $PGO_PROFILES_DIR" + fi +} + +# Parse arguments +STAGE="" +JOBS=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo "4") +ENABLE_LTO="OFF" + +while [[ $# -gt 0 ]]; do + case $1 in + generate|use|clean|merge|summary) + STAGE="$1" + shift + ;; + -j) + JOBS="$2" + shift 2 + ;; + -lto) + ENABLE_LTO="ON" + shift + ;; + -h|--help) + show_usage + exit 0 + ;; + *) + print_error "Unknown option: $1" + show_usage + exit 1 + ;; + esac +done + +if [ -z "$STAGE" ]; then + print_error "No stage specified" + show_usage + exit 1 +fi + +# --- Clean stage --- +if [ "$STAGE" == "clean" ]; then + print_header "Cleaning Build Directory" + + if [ -d "$PGO_PROFILES_DIR" ]; then + print_info "Backing up PGO profiles..." + mkdir -p "$BACKUP_PROFILES_DIR" + cp -r "$PGO_PROFILES_DIR"/* "$BACKUP_PROFILES_DIR/" 2>/dev/null || true + fi + + if [ -d "$BUILD_DIR" ]; then + print_info "Removing build directory..." + rm -rf "$BUILD_DIR" + fi + + if [ -d "$BACKUP_PROFILES_DIR" ]; then + print_info "Restoring PGO profiles..." + mkdir -p "$PGO_PROFILES_DIR" + mv "$BACKUP_PROFILES_DIR"/* "$PGO_PROFILES_DIR/" 2>/dev/null || true + rm -rf "$BACKUP_PROFILES_DIR" + fi + + print_info "Clean complete!" + exit 0 +fi + +# --- Merge stage --- +if [ "$STAGE" == "merge" ]; then + print_header "Merging PGO Profiles" + + if [ ! -d "$PGO_PROFILES_DIR" ]; then + print_error "Profile directory not found: $PGO_PROFILES_DIR" + exit 1 + fi + + merge_clang_profiles + print_info "Done! Run '$0 summary' to check coverage." + exit 0 +fi + +# --- Summary stage --- +if [ "$STAGE" == "summary" ]; then + show_profile_summary + exit 0 +fi + +# --- Generate stage --- +if [ "$STAGE" == "generate" ]; then + print_header "PGO Stage 1: Generate Profile Data" + + # Clean stale profile data from any previous run + if [ -d "$PGO_PROFILES_DIR" ]; then + clean_stale_profiles + fi + + # Create build directory + mkdir -p "$BUILD_DIR" + cd "$BUILD_DIR" + + # Configure + print_info "Configuring CMake..." + cmake .. \ + -DCITRON_ENABLE_PGO_GENERATE=ON \ + -DCITRON_ENABLE_LTO=$ENABLE_LTO \ + -DCMAKE_BUILD_TYPE=Release + + # Build + print_info "Building instrumented Citron (this may take a while)..." + cmake --build . -j"$JOBS" + + print_header "Build Complete!" + echo "" + echo -e "${YELLOW} Training guide for best PGO results:${NC}" + echo "" + echo " 1. Run: ./bin/citron" + echo " 2. Launch a game and play PAST initial loading" + echo " (first-time shader compilation is a critical hot path)" + echo " 3. Play for at least 5-10 minutes per game" + echo " 4. Test 2-3 different games for broader code coverage" + echo " 5. Navigate menus, settings, and game list to profile the UI" + echo " 6. Exit citron cleanly (File -> Exit or Ctrl+Q)" + echo "" + echo -e "${YELLOW} After each session, you can run:${NC}" + echo " $0 merge # Consolidate collected profiles" + echo " $0 summary # Check profile coverage" + echo "" + echo -e "${YELLOW} When satisfied with coverage, build the optimized binary:${NC}" + echo " $0 use" + echo "" +fi + +# --- Use stage --- +if [ "$STAGE" == "use" ]; then + print_header "PGO Stage 2: Build Optimized Binary" + + # Check if profile data exists + if [ ! -d "$PGO_PROFILES_DIR" ] || [ -z "$(ls -A $PGO_PROFILES_DIR 2>/dev/null)" ]; then + print_error "No profile data found in $PGO_PROFILES_DIR" + print_info "Please run the generate stage first and collect profile data" + exit 1 + fi + + # Merge any outstanding raw profiles before building + print_info "Merging any outstanding profile data..." + merge_clang_profiles || true + + # Backup profiles if build directory exists + if [ -d "$BUILD_DIR" ]; then + print_info "Backing up PGO profiles..." + mkdir -p "$BACKUP_PROFILES_DIR" + cp -r "$PGO_PROFILES_DIR"/* "$BACKUP_PROFILES_DIR/" + rm -rf "$BUILD_DIR" + fi + + # Create build directory and restore profiles + mkdir -p "$BUILD_DIR" + if [ -d "$BACKUP_PROFILES_DIR" ]; then + mkdir -p "$PGO_PROFILES_DIR" + mv "$BACKUP_PROFILES_DIR"/* "$PGO_PROFILES_DIR/" + rm -rf "$BACKUP_PROFILES_DIR" + fi + + cd "$BUILD_DIR" + + # Configure + print_info "Configuring CMake..." + cmake .. \ + -DCITRON_ENABLE_PGO_USE=ON \ + -DCITRON_ENABLE_LTO=$ENABLE_LTO \ + -DCMAKE_BUILD_TYPE=Release + + # Build + print_info "Building optimized Citron (this may take a while)..." + cmake --build . -j"$JOBS" + + print_header "Build Complete!" + print_info "Your optimized Citron binary is ready!" + print_info "Location: $BUILD_DIR/bin/citron" + echo "" + print_info "This build is optimized for your specific usage patterns." +fi