diff --git a/.github/workflows/build-appimage.yml b/.github/workflows/build-appimage.yml index bd24143f..ccc3e37b 100644 --- a/.github/workflows/build-appimage.yml +++ b/.github/workflows/build-appimage.yml @@ -51,6 +51,9 @@ jobs: env: # appimagetool runs without FUSE on CI runners APPIMAGE_EXTRACT_AND_RUN: '1' + # Keep AppImages compatible with distributions that provide glibc + # 2.36. The build script rejects artifacts linked against newer versions. + MAX_GLIBC_VERSION: '2.36' PYTHON: python run: ./packaging/linux/build_appimage.sh diff --git a/Module/appconfig.py b/Module/appconfig.py index c5f6eb02..c44aee14 100644 --- a/Module/appconfig.py +++ b/Module/appconfig.py @@ -84,6 +84,25 @@ def write_openkh_path(selected_directory): update_app_config('openkh_folder', selected_directory) +def is_openkh_folder(selected_path: Path) -> bool: + """Accept both extracted OpenKH installations and Linux config folders.""" + mods_manager_names = ( + "OpenKh.Tools.ModsManager.exe", + "OpenKh.Tools.ModsManager", + "OpenKh.Tools.ModsManager.Avalonia", + ) + tool_directories = (selected_path, selected_path / "Apps") + has_mods_manager = any( + (tool_directory / name).is_file() + for tool_directory in tool_directories + for name in mods_manager_names + ) + + # Native Linux installations keep Mods Manager data in ~/.config/OpenKh. + has_mods_manager_config = (selected_path / "mods-manager.yml").is_file() + return has_mods_manager or has_mods_manager_config + + def read_custom_music_path() -> Optional[Path]: return _read_directory("custom_music_folder") diff --git a/UI/configui.py b/UI/configui.py index c0377f00..7c6d09fc 100644 --- a/UI/configui.py +++ b/UI/configui.py @@ -20,17 +20,12 @@ def openkh_folder_getter() -> bool: return False selected_path = Path(selected_directory) - mods_manager_names = ( - "OpenKh.Tools.ModsManager.exe", - "OpenKh.Tools.ModsManager", - "OpenKh.Tools.ModsManager.Avalonia", - ) - if not any((selected_path / name).is_file() for name in mods_manager_names): + if not appconfig.is_openkh_folder(selected_path): show_alert("Not a valid OpenKH folder.") return False - else: - appconfig.write_openkh_path(selected_directory) - return True + + appconfig.write_openkh_path(selected_directory) + return True def custom_music_folder_getter() -> bool: diff --git a/packaging/linux/build_appimage.sh b/packaging/linux/build_appimage.sh index 8d1224a9..f3e178b6 100755 --- a/packaging/linux/build_appimage.sh +++ b/packaging/linux/build_appimage.sh @@ -18,6 +18,7 @@ BUILD_DIR=build/appimage APPDIR="$BUILD_DIR/AppDir" OUTPUT_NAME="KH2.Randomizer-x86_64.AppImage" APPIMAGETOOL_SHA256="a6d71e2b6cd66f8e8d16c37ad164658985e0cf5fcaa950c90a482890cb9d13e0" +MAX_GLIBC_VERSION=${MAX_GLIBC_VERSION:-2.36} if [ ! -f extracted_data.zip ]; then echo "error: extracted_data.zip not found in the repo root (required for bundling)" >&2 @@ -34,6 +35,10 @@ cp packaging/linux/kh2randomizer.desktop "$APPDIR/" "$PYTHON" -c "from PIL import Image; Image.open('rando.ico').save('$APPDIR/kh2randomizer.png')" cp "$APPDIR/kh2randomizer.png" "$APPDIR/usr/share/icons/hicolor/256x256/apps/kh2randomizer.png" +# Fail before packaging if a local or CI build accidentally raises the Linux +# runtime requirement above the compatibility target. +packaging/linux/check_glibc_compatibility.sh "$APPDIR" "$MAX_GLIBC_VERSION" + APPIMAGETOOL=${APPIMAGETOOL:-appimagetool} if ! command -v "$APPIMAGETOOL" >/dev/null 2>&1; then APPIMAGETOOL="$BUILD_DIR/appimagetool-x86_64.AppImage" diff --git a/packaging/linux/check_glibc_compatibility.sh b/packaging/linux/check_glibc_compatibility.sh new file mode 100755 index 00000000..f7022c4c --- /dev/null +++ b/packaging/linux/check_glibc_compatibility.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Verifies that every ELF binary in a directory supports the requested glibc. +set -euo pipefail + +ROOT=${1:?usage: check_glibc_compatibility.sh DIRECTORY [MAX_GLIBC_VERSION]} +MAX_GLIBC_VERSION=${2:-2.36} +highest_version=0 +highest_file= + +while IFS= read -r -d '' file; do + if ! readelf -h "$file" >/dev/null 2>&1; then + continue + fi + + version=$(readelf --version-info "$file" 2>/dev/null \ + | grep -oE 'GLIBC_[0-9]+\.[0-9]+' \ + | sed 's/^GLIBC_//' \ + | sort -V \ + | tail -n 1 || true) + + if [ -n "$version" ] && [ "$(printf '%s\n%s\n' "$highest_version" "$version" | sort -V | tail -n 1)" = "$version" ]; then + highest_version=$version + highest_file=$file + fi +done < <(find "$ROOT" -type f -print0) + +echo "Highest required glibc version: $highest_version${highest_file:+ ($highest_file)}" + +if [ "$(printf '%s\n%s\n' "$MAX_GLIBC_VERSION" "$highest_version" | sort -V | tail -n 1)" != "$MAX_GLIBC_VERSION" ]; then + echo "error: build requires glibc $highest_version, newer than supported glibc $MAX_GLIBC_VERSION" >&2 + exit 1 +fi diff --git a/tests/test_configui.py b/tests/test_configui.py new file mode 100644 index 00000000..0aca5660 --- /dev/null +++ b/tests/test_configui.py @@ -0,0 +1,19 @@ +from Module import appconfig + + +def test_openkh_folder_accepts_linux_config_directory(tmp_path): + (tmp_path / "mods-manager.yml").write_text("installedModsPath: /tmp/mods\n") + + assert appconfig.is_openkh_folder(tmp_path) + + +def test_openkh_folder_accepts_mods_manager_in_apps(tmp_path): + apps_path = tmp_path / "Apps" + apps_path.mkdir() + (apps_path / "OpenKh.Tools.ModsManager").write_text("tool") + + assert appconfig.is_openkh_folder(tmp_path) + + +def test_openkh_folder_rejects_unrelated_directory(tmp_path): + assert not appconfig.is_openkh_folder(tmp_path)