aboutsummaryrefslogtreecommitdiff
path: root/libs
diff options
context:
space:
mode:
Diffstat (limited to 'libs')
-rw-r--r--libs/nak/src/game_finder/heroic.rs23
-rw-r--r--libs/nak/src/game_finder/known_games.rs159
-rw-r--r--libs/nak/src/game_finder/mod.rs31
-rw-r--r--libs/nak_ffi/include/nak_ffi.h1
-rw-r--r--libs/nak_ffi/src/lib.rs2
-rw-r--r--libs/plugin_python/src/runner/pythonrunner.cpp102
6 files changed, 256 insertions, 62 deletions
diff --git a/libs/nak/src/game_finder/heroic.rs b/libs/nak/src/game_finder/heroic.rs
index 7bdda09..e80c1e1 100644
--- a/libs/nak/src/game_finder/heroic.rs
+++ b/libs/nak/src/game_finder/heroic.rs
@@ -8,7 +8,7 @@ use std::path::{Path, PathBuf};
use serde::Deserialize;
-use super::known_games::find_by_gog_id;
+use super::known_games::{find_by_epic_id, find_by_gog_id, find_by_title};
use super::{Game, HeroicStore, Launcher};
use crate::logging::{log_info, log_warning};
@@ -202,11 +202,16 @@ fn detect_epic_games(heroic_path: &Path) -> Vec<Game> {
}
// Get title
- let name = game_obj
+ let title = game_obj
.get("title")
.and_then(|v| v.as_str())
- .unwrap_or(app_name)
- .to_string();
+ .unwrap_or(app_name);
+
+ // Look up known game info: try Epic AppName first, then title match
+ let known_game = find_by_epic_id(app_name)
+ .or_else(|| find_by_title(title));
+
+ let name = title.to_string();
// Get Wine prefix
let prefix_path = get_heroic_game_prefix(heroic_path, app_name);
@@ -219,11 +224,11 @@ fn detect_epic_games(heroic_path: &Path) -> Vec<Game> {
launcher: Launcher::Heroic {
store: HeroicStore::Epic,
},
- my_games_folder: None,
- appdata_local_folder: None,
- appdata_roaming_folder: None,
- registry_path: None,
- registry_value: None,
+ my_games_folder: known_game.and_then(|g| g.my_games_folder.map(String::from)),
+ appdata_local_folder: known_game.and_then(|g| g.appdata_local_folder.map(String::from)),
+ appdata_roaming_folder: known_game.and_then(|g| g.appdata_roaming_folder.map(String::from)),
+ registry_path: known_game.map(|g| g.registry_path.to_string()),
+ registry_value: known_game.map(|g| g.registry_value.to_string()),
});
}
}
diff --git a/libs/nak/src/game_finder/known_games.rs b/libs/nak/src/game_finder/known_games.rs
index 8a30c18..117b0c7 100644
--- a/libs/nak/src/game_finder/known_games.rs
+++ b/libs/nak/src/game_finder/known_games.rs
@@ -2,6 +2,8 @@
//!
//! Contains metadata for games that NaK supports, including:
//! - Steam App ID
+//! - GOG App ID
+//! - Epic Games Store App Name (Legendary/Heroic internal identifier)
//! - My Games folder name (Documents/My Games/*)
//! - AppData/Local folder name
//! - Registry path for game detection
@@ -13,8 +15,10 @@ pub struct KnownGame {
pub name: &'static str,
/// Steam App ID
pub steam_app_id: &'static str,
- /// GOG App ID (if available)
+ /// GOG App ID (if available on GOG)
pub gog_app_id: Option<&'static str>,
+ /// Epic Games Store AppName / Legendary internal ID (if available on Epic)
+ pub epic_app_id: Option<&'static str>,
/// Folder name in Documents/My Games (if applicable)
pub my_games_folder: Option<&'static str>,
/// Folder name in AppData/Local (if applicable)
@@ -35,7 +39,8 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
KnownGame {
name: "Enderal",
steam_app_id: "933480",
- gog_app_id: None,
+ gog_app_id: Some("1708684988"), // Enderal: Forgotten Stories on GOG
+ epic_app_id: None,
my_games_folder: Some("Enderal"),
appdata_local_folder: None,
appdata_roaming_folder: None,
@@ -46,7 +51,8 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
KnownGame {
name: "Enderal Special Edition",
steam_app_id: "976620",
- gog_app_id: None,
+ gog_app_id: None, // Not on GOG (only original Enderal)
+ epic_app_id: None,
my_games_folder: Some("Enderal Special Edition"),
appdata_local_folder: None,
appdata_roaming_folder: None,
@@ -58,6 +64,7 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
name: "Fallout 3",
steam_app_id: "22300",
gog_app_id: Some("1454315831"), // Fallout 3 GOTY
+ epic_app_id: Some("adeae8bbfc94427db57c7dfecce3f1d4"),
my_games_folder: Some("Fallout3"),
appdata_local_folder: Some("Fallout3"),
appdata_roaming_folder: None,
@@ -68,7 +75,8 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
KnownGame {
name: "Fallout 4",
steam_app_id: "377160",
- gog_app_id: None,
+ gog_app_id: Some("1998527297"), // Fallout 4 GOTY on GOG
+ epic_app_id: Some("61d52ce4d09d41e48800c22784d13ae8"),
my_games_folder: Some("Fallout4"),
appdata_local_folder: Some("Fallout4"),
appdata_roaming_folder: None,
@@ -80,6 +88,7 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
name: "Fallout 4 VR",
steam_app_id: "611660",
gog_app_id: None,
+ epic_app_id: None,
my_games_folder: Some("Fallout4VR"),
appdata_local_folder: None,
appdata_roaming_folder: None,
@@ -91,6 +100,7 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
name: "Fallout New Vegas",
steam_app_id: "22380",
gog_app_id: Some("1454587428"), // Fallout NV Ultimate
+ epic_app_id: Some("5daeb974a22a435988892319b3a4f476"),
my_games_folder: Some("FalloutNV"),
appdata_local_folder: Some("FalloutNV"),
appdata_roaming_folder: None,
@@ -102,6 +112,7 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
name: "Morrowind",
steam_app_id: "22320",
gog_app_id: Some("1440163901"), // Morrowind GOTY
+ epic_app_id: None, // On Epic but internal AppName not yet known
my_games_folder: Some("Morrowind"),
appdata_local_folder: None,
appdata_roaming_folder: None,
@@ -113,6 +124,7 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
name: "Oblivion",
steam_app_id: "22330",
gog_app_id: Some("1458058109"), // Oblivion GOTY Deluxe
+ epic_app_id: None, // On Epic but internal AppName not yet known
my_games_folder: Some("Oblivion"),
appdata_local_folder: Some("Oblivion"),
appdata_roaming_folder: None,
@@ -123,7 +135,8 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
KnownGame {
name: "Skyrim",
steam_app_id: "72850",
- gog_app_id: None, // Not on GOG
+ gog_app_id: None, // Not on GOG (only SE/AE)
+ epic_app_id: None, // Not on Epic (only SE/AE)
my_games_folder: Some("Skyrim"),
appdata_local_folder: Some("Skyrim"),
appdata_roaming_folder: None,
@@ -134,7 +147,8 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
KnownGame {
name: "Skyrim Special Edition",
steam_app_id: "489830",
- gog_app_id: Some("1711230643"), // Skyrim SE Anniversary Edition
+ gog_app_id: Some("1711230643"), // Skyrim SE on GOG
+ epic_app_id: Some("ac82db5035584c7f8a2c548d98c86b2c"),
my_games_folder: Some("Skyrim Special Edition"),
appdata_local_folder: Some("Skyrim Special Edition"),
appdata_roaming_folder: None,
@@ -146,6 +160,7 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
name: "Skyrim VR",
steam_app_id: "611670",
gog_app_id: None,
+ epic_app_id: None,
my_games_folder: Some("Skyrim VR"),
appdata_local_folder: None,
appdata_roaming_folder: None,
@@ -156,7 +171,8 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
KnownGame {
name: "Starfield",
steam_app_id: "1716740",
- gog_app_id: None,
+ gog_app_id: None, // Xbox/Steam exclusive
+ epic_app_id: None,
my_games_folder: Some("Starfield"),
appdata_local_folder: None,
appdata_roaming_folder: None,
@@ -169,6 +185,7 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
name: "The Witcher 3",
steam_app_id: "292030",
gog_app_id: Some("1495134320"), // Witcher 3 GOTY
+ epic_app_id: None,
my_games_folder: Some("The Witcher 3"),
appdata_local_folder: None,
appdata_roaming_folder: None,
@@ -180,6 +197,7 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
name: "Cyberpunk 2077",
steam_app_id: "1091500",
gog_app_id: Some("1423049311"),
+ epic_app_id: None,
my_games_folder: None,
appdata_local_folder: Some("CD Projekt Red/Cyberpunk 2077"),
appdata_roaming_folder: None,
@@ -192,6 +210,7 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
name: "Baldur's Gate 3",
steam_app_id: "1086940",
gog_app_id: Some("1456460669"),
+ epic_app_id: None,
my_games_folder: None,
appdata_local_folder: Some("Larian Studios/Baldur's Gate 3"),
appdata_roaming_folder: None,
@@ -201,6 +220,15 @@ pub const KNOWN_GAMES: &[KnownGame] = &[
},
];
+/// Alternative GOG App IDs that should map to the same game.
+/// Some games have multiple GOG product IDs (e.g. different editions).
+const GOG_ID_ALIASES: &[(&str, &str)] = &[
+ // Morrowind: Vortex uses 1435828767, NaK primary is 1440163901
+ ("1435828767", "1440163901"),
+ // Skyrim Anniversary Edition is a separate GOG product but same game as SE
+ ("1801825368", "1711230643"),
+];
+
/// Find a known game by Steam App ID
pub fn find_by_steam_id(app_id: &str) -> Option<&'static KnownGame> {
let normalized_id = normalize_steam_id(app_id);
@@ -209,9 +237,79 @@ pub fn find_by_steam_id(app_id: &str) -> Option<&'static KnownGame> {
/// Find a known game by GOG App ID
pub fn find_by_gog_id(app_id: &str) -> Option<&'static KnownGame> {
+ // Direct match first
+ if let Some(game) = KNOWN_GAMES.iter().find(|g| g.gog_app_id == Some(app_id)) {
+ return Some(game);
+ }
+ // Check aliases: resolve alternate GOG ID to primary, then look up
+ for &(alias, primary) in GOG_ID_ALIASES {
+ if app_id == alias {
+ return KNOWN_GAMES.iter().find(|g| g.gog_app_id == Some(primary));
+ }
+ }
+ None
+}
+
+/// Find a known game by Epic Games Store AppName (Legendary/Heroic internal ID)
+pub fn find_by_epic_id(app_id: &str) -> Option<&'static KnownGame> {
KNOWN_GAMES
.iter()
- .find(|g| g.gog_app_id == Some(app_id))
+ .find(|g| g.epic_app_id == Some(app_id))
+}
+
+/// Strip punctuation and collapse whitespace for fuzzy title matching.
+fn normalize_for_matching(s: &str) -> String {
+ s.chars()
+ .map(|c| if c.is_alphanumeric() || c == ' ' { c } else { ' ' })
+ .collect::<String>()
+ .split_whitespace()
+ .collect::<Vec<_>>()
+ .join(" ")
+}
+
+/// Find a known game by title, using fuzzy matching.
+/// Strips common suffixes like "Game of the Year Edition" for comparison.
+pub fn find_by_title(title: &str) -> Option<&'static KnownGame> {
+ let title_lower = title.to_lowercase();
+
+ // Exact match first
+ if let Some(game) = KNOWN_GAMES.iter().find(|g| g.name.to_lowercase() == title_lower) {
+ return Some(game);
+ }
+
+ // Check if the title starts with or contains a known game name
+ // Sort by name length descending so "Skyrim Special Edition" matches before "Skyrim"
+ let mut games_by_name_len: Vec<&KnownGame> = KNOWN_GAMES.iter().collect();
+ games_by_name_len.sort_by(|a, b| b.name.len().cmp(&a.name.len()));
+
+ // Normalize both title and game names: strip colons and extra whitespace for comparison
+ let title_normalized = normalize_for_matching(&title_lower);
+
+ for game in games_by_name_len {
+ let game_lower = game.name.to_lowercase();
+ let game_normalized = normalize_for_matching(&game_lower);
+
+ // Direct containment (e.g. "Fallout 3: Game of the Year Edition" contains "Fallout 3")
+ if title_lower.contains(&game_lower) {
+ return Some(game);
+ }
+
+ // Normalized containment (e.g. "Fallout: New Vegas" normalized to "fallout new vegas"
+ // matches game name "Fallout New Vegas" normalized to "fallout new vegas")
+ if title_normalized.contains(&game_normalized) {
+ return Some(game);
+ }
+
+ // After-colon match: "The Elder Scrolls III: Morrowind" -> check "Morrowind"
+ if let Some(after_colon) = title_lower.split(':').nth(1) {
+ let after_colon = after_colon.trim();
+ if after_colon.starts_with(&game_lower) {
+ return Some(game);
+ }
+ }
+ }
+
+ None
}
/// Find a known game by name (case-insensitive)
@@ -234,7 +332,7 @@ fn normalize_steam_id(app_id: &str) -> &str {
#[cfg(test)]
mod tests {
- use super::find_by_steam_id;
+ use super::*;
#[test]
fn fallout_3_goty_alias_maps_to_fallout_3() {
@@ -242,4 +340,47 @@ mod tests {
assert_eq!(game.name, "Fallout 3");
assert_eq!(game.steam_app_id, "22300");
}
+
+ #[test]
+ fn find_by_epic_id_works() {
+ let game = find_by_epic_id("ac82db5035584c7f8a2c548d98c86b2c")
+ .expect("Should find Skyrim SE by Epic ID");
+ assert_eq!(game.name, "Skyrim Special Edition");
+ }
+
+ #[test]
+ fn gog_alias_maps_correctly() {
+ // Skyrim Anniversary Edition GOG ID maps to Skyrim SE
+ let game = find_by_gog_id("1801825368")
+ .expect("Skyrim AE GOG ID should map to Skyrim SE");
+ assert_eq!(game.name, "Skyrim Special Edition");
+ }
+
+ #[test]
+ fn find_by_title_matches_full_titles() {
+ let game = find_by_title("The Elder Scrolls III: Morrowind Game of the Year Edition")
+ .expect("Should find Morrowind by full Epic title");
+ assert_eq!(game.name, "Morrowind");
+ }
+
+ #[test]
+ fn find_by_title_matches_colon_titles() {
+ let game = find_by_title("The Elder Scrolls IV: Oblivion Game of the Year Edition")
+ .expect("Should find Oblivion by full title");
+ assert_eq!(game.name, "Oblivion");
+ }
+
+ #[test]
+ fn find_by_title_prefers_longer_match() {
+ let game = find_by_title("Skyrim Special Edition")
+ .expect("Should find Skyrim SE, not Skyrim");
+ assert_eq!(game.name, "Skyrim Special Edition");
+ }
+
+ #[test]
+ fn find_by_title_fallout_nv() {
+ let game = find_by_title("Fallout: New Vegas Ultimate Edition")
+ .expect("Should find Fallout NV");
+ assert_eq!(game.name, "Fallout New Vegas");
+ }
}
diff --git a/libs/nak/src/game_finder/mod.rs b/libs/nak/src/game_finder/mod.rs
index c383c8a..600751a 100644
--- a/libs/nak/src/game_finder/mod.rs
+++ b/libs/nak/src/game_finder/mod.rs
@@ -20,7 +20,10 @@ use std::path::PathBuf;
pub use bottles::detect_bottles_games;
pub use heroic::detect_heroic_games;
-pub use known_games::{find_by_gog_id, find_by_name, find_by_steam_id, KnownGame, KNOWN_GAMES};
+pub use known_games::{
+ find_by_epic_id, find_by_gog_id, find_by_name, find_by_steam_id, find_by_title, KnownGame,
+ KNOWN_GAMES,
+};
pub use registry::{read_registry_value, wine_path_to_linux};
pub use steam::{detect_steam_games, find_game_install_path, find_game_prefix_path, get_known_game};
@@ -161,10 +164,16 @@ impl GameScanResult {
// Public API
// ============================================================================
-/// Detect all installed games from all supported launchers
+/// Detect all installed games from all supported launchers.
+///
+/// Games are detected in priority order: Steam -> Heroic (GOG -> Epic) -> Bottles.
+/// When the same game is found from multiple launchers, the higher-priority
+/// detection is kept and duplicates are skipped. This ensures registry entries
+/// (which are shared across storefronts) prefer Steam paths, then GOG, then Epic.
pub fn detect_all_games() -> GameScanResult {
let mut result = GameScanResult::default();
+ // Detect in priority order: Steam first, then GOG (via Heroic), then Epic, then Bottles.
let steam_games = detect_steam_games();
result.steam_count = steam_games.len();
result.games.extend(steam_games);
@@ -177,9 +186,27 @@ pub fn detect_all_games() -> GameScanResult {
result.bottles_count = bottles_games.len();
result.games.extend(bottles_games);
+ // Deduplicate: keep the first occurrence of each game (by registry_path),
+ // which respects the detection order (Steam > GOG > Epic > Bottles).
+ deduplicate_games(&mut result);
+
result
}
+/// Remove duplicate game detections, keeping the first (highest-priority) entry.
+/// Two games are considered duplicates if they have the same registry_path.
+fn deduplicate_games(result: &mut GameScanResult) {
+ let mut seen_registry_paths = std::collections::HashSet::new();
+ result.games.retain(|game| {
+ if let Some(ref reg_path) = game.registry_path {
+ seen_registry_paths.insert(reg_path.clone())
+ } else {
+ // Games without registry paths are always kept (no risk of conflict)
+ true
+ }
+ });
+}
+
/// Detect only Steam games
pub fn detect_steam_only() -> GameScanResult {
let steam_games = detect_steam_games();
diff --git a/libs/nak_ffi/include/nak_ffi.h b/libs/nak_ffi/include/nak_ffi.h
index 8e24041..cc66d9e 100644
--- a/libs/nak_ffi/include/nak_ffi.h
+++ b/libs/nak_ffi/include/nak_ffi.h
@@ -46,6 +46,7 @@ typedef struct {
const char *name;
const char *steam_app_id;
const char *gog_app_id; /* NULL if none */
+ const char *epic_app_id; /* NULL if none */
const char *my_games_folder; /* NULL if not applicable */
const char *appdata_local_folder; /* NULL if not applicable */
const char *appdata_roaming_folder; /* NULL if not applicable */
diff --git a/libs/nak_ffi/src/lib.rs b/libs/nak_ffi/src/lib.rs
index fe18cf3..65592fa 100644
--- a/libs/nak_ffi/src/lib.rs
+++ b/libs/nak_ffi/src/lib.rs
@@ -212,6 +212,7 @@ pub struct NakKnownGame {
pub name: *const c_char,
pub steam_app_id: *const c_char,
pub gog_app_id: *const c_char, // null if none
+ pub epic_app_id: *const c_char, // null if none
pub my_games_folder: *const c_char,
pub appdata_local_folder: *const c_char,
pub appdata_roaming_folder: *const c_char,
@@ -236,6 +237,7 @@ static KNOWN_GAMES_FFI: std::sync::LazyLock<KnownGamesVec> = std::sync::LazyLock
name: leak_str(kg.name),
steam_app_id: leak_str(kg.steam_app_id),
gog_app_id: leak_str_opt(kg.gog_app_id),
+ epic_app_id: leak_str_opt(kg.epic_app_id),
my_games_folder: leak_str_opt(kg.my_games_folder),
appdata_local_folder: leak_str_opt(kg.appdata_local_folder),
appdata_roaming_folder: leak_str_opt(kg.appdata_roaming_folder),
diff --git a/libs/plugin_python/src/runner/pythonrunner.cpp b/libs/plugin_python/src/runner/pythonrunner.cpp
index b0bf48c..3bc62a1 100644
--- a/libs/plugin_python/src/runner/pythonrunner.cpp
+++ b/libs/plugin_python/src/runner/pythonrunner.cpp
@@ -89,55 +89,63 @@ namespace mo2::python {
static const char* argv0 = "ModOrganizer.exe";
#ifndef _WIN32
-#ifdef MO2_PYTHON_SHARED_LIBRARY
// Ensure libpython symbols are globally visible for extension modules
// loaded later (_struct, PyQt6, etc.).
- bool usedNoload = true;
- void* pyHandle =
- dlopen(MO2_PYTHON_SHARED_LIBRARY, RTLD_NOW | RTLD_GLOBAL | RTLD_NOLOAD);
- if (pyHandle == nullptr) {
- usedNoload = false;
- pyHandle = dlopen(MO2_PYTHON_SHARED_LIBRARY, RTLD_NOW | RTLD_GLOBAL);
- }
- if (pyHandle == nullptr) {
- MOBase::log::warn("python: failed to dlopen '{}': {}",
- MO2_PYTHON_SHARED_LIBRARY, dlerror());
- } else {
- MOBase::log::debug("python: '{}' promoted to RTLD_GLOBAL ({})",
- MO2_PYTHON_SHARED_LIBRARY,
- usedNoload ? "RTLD_NOLOAD" : "fresh load");
- }
-
- // Diagnose which DSO the Python init symbols resolve to globally.
+ //
+ // We must promote the *already-loaded* libpython to RTLD_GLOBAL.
+ // Using the compile-time filename (e.g. "libpython3.13.so.1.0") with
+ // RTLD_NOLOAD can fail when the portable Python's SONAME differs
+ // (e.g. "libpython3.13.so"), causing a second copy to be loaded and
+ // making Py_IsInitialized() return false after Py_InitializeFromConfig().
+ //
+ // Instead, find the DSO that provides Py_IsInitialized via dladdr, then
+ // re-dlopen that exact path with RTLD_GLOBAL.
{
Dl_info di;
- void* sym = dlsym(RTLD_DEFAULT, "Py_InitializeEx");
- if (sym && dladdr(sym, &di)) {
- fprintf(stderr, "[py-diag] Py_InitializeEx global from '%s'\n",
+ void* sym = dlsym(RTLD_DEFAULT, "Py_IsInitialized");
+ if (sym && dladdr(sym, &di) && di.dli_fname) {
+ void* pyHandle =
+ dlopen(di.dli_fname, RTLD_NOW | RTLD_GLOBAL | RTLD_NOLOAD);
+ if (pyHandle) {
+ MOBase::log::debug(
+ "python: promoted '{}' to RTLD_GLOBAL via dladdr",
di.dli_fname);
- MOBase::log::debug("python: Py_InitializeEx (global) from '{}'",
- di.dli_fname);
- } else {
- fprintf(stderr, "[py-diag] Py_InitializeEx NOT in global scope "
- "(sym=%p, err=%s)\n",
- sym, dlerror());
- MOBase::log::warn("python: Py_InitializeEx NOT in global scope "
- "(sym={}, err={})",
- sym, dlerror());
- }
- sym = dlsym(RTLD_DEFAULT, "Py_IsInitialized");
- if (sym && dladdr(sym, &di)) {
- fprintf(stderr, "[py-diag] Py_IsInitialized global from '%s'\n",
- di.dli_fname);
- MOBase::log::debug("python: Py_IsInitialized (global) from '{}'",
- di.dli_fname);
+ } else {
+ // Fallback: load by full path (not NOLOAD).
+ pyHandle = dlopen(di.dli_fname, RTLD_NOW | RTLD_GLOBAL);
+ if (pyHandle) {
+ MOBase::log::debug(
+ "python: loaded '{}' with RTLD_GLOBAL (fresh)",
+ di.dli_fname);
+ } else {
+ MOBase::log::warn(
+ "python: failed to promote '{}' to RTLD_GLOBAL: {}",
+ di.dli_fname, dlerror());
+ }
+ }
} else {
- fprintf(stderr, "[py-diag] Py_IsInitialized NOT in global scope\n");
- MOBase::log::warn("python: Py_IsInitialized NOT in global scope");
+ // Py_IsInitialized not yet in scope — libpython may not be loaded
+ // as a dependency yet. Try the compile-time name.
+#ifdef MO2_PYTHON_SHARED_LIBRARY
+ void* pyHandle =
+ dlopen(MO2_PYTHON_SHARED_LIBRARY, RTLD_NOW | RTLD_GLOBAL);
+ if (pyHandle) {
+ MOBase::log::debug(
+ "python: loaded '{}' with RTLD_GLOBAL (compile-time name)",
+ MO2_PYTHON_SHARED_LIBRARY);
+ } else {
+ MOBase::log::warn(
+ "python: failed to dlopen '{}': {}",
+ MO2_PYTHON_SHARED_LIBRARY, dlerror());
+ }
+#else
+ MOBase::log::warn(
+ "python: Py_IsInitialized not found in global scope and "
+ "no compile-time library name available");
+#endif
}
}
#endif
-#endif
// For portable/AppImage builds, set PYTHONHOME so the interpreter
// finds the bundled stdlib instead of looking at system paths.
@@ -221,8 +229,18 @@ namespace mo2::python {
{
PyConfig config;
PyConfig_InitPythonConfig(&config);
- // PYTHONHOME is already set via setenv; PyConfig reads it from env.
- PyStatus status = Py_InitializeFromConfig(&config);
+ // Set config.home directly (more reliable than env for embedded use).
+ std::wstring wHome = pythonHome.toStdWString();
+ PyStatus status = PyConfig_SetString(&config, &config.home, wHome.c_str());
+ if (PyStatus_Exception(status)) {
+ MOBase::log::error(
+ "python: PyConfig_SetString(home) failed: '{}'",
+ status.err_msg ? status.err_msg : "(no message)");
+ PyConfig_Clear(&config);
+ restorePythonEnv();
+ return false;
+ }
+ status = Py_InitializeFromConfig(&config);
PyConfig_Clear(&config);
if (PyStatus_Exception(status)) {
fprintf(stderr,