Skip to content

Commit 3a00642

Browse files
committed
Enhance Python module library detection and path resolution
- Improve Python library and executable detection across platforms - Add robust method to find Python executable using system commands - Implement more comprehensive library path searching - Support multiple Python versions and environment configurations - Add better error handling for Python path and library probing - Optimize library candidate generation for Windows, macOS, and Linux
1 parent d24990b commit 3a00642

File tree

1 file changed

+177
-32
lines changed

1 file changed

+177
-32
lines changed

shards/modules/py/py.cpp

+177-32
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
#ifndef CB_PYTHON_HPP
22
#define CB_PYTHON_HPP
33

4-
// must be on top
5-
#ifndef __kernel_entry
6-
#define __kernel_entry
7-
#endif
8-
#include <boost/process.hpp>
9-
104
#include <shards/core/platform.hpp>
115
#include <shards/core/module.hpp>
126
#include <shards/core/foundation.hpp>
@@ -20,6 +14,10 @@
2014
#include <dlfcn.h>
2115
#endif
2216

17+
// For popen
18+
#include <cstdio>
19+
#include <array>
20+
2321
// define ssize_t on windows
2422
#if _WIN32
2523
#include <BaseTsd.h>
@@ -327,24 +325,94 @@ struct Env {
327325

328326
static inline std::vector<std::string> Path;
329327

328+
static std::string getPythonCommand() {
329+
#ifdef _WIN32
330+
// On Windows, try python.exe first, then py.exe (Python launcher)
331+
std::array<char, 1024> buffer;
332+
FILE *pipe = _popen("where python.exe", "r");
333+
if (pipe) {
334+
if (fgets(buffer.data(), buffer.size(), pipe) != nullptr) {
335+
_pclose(pipe);
336+
return "python.exe";
337+
}
338+
_pclose(pipe);
339+
}
340+
341+
pipe = _popen("where py.exe", "r");
342+
if (pipe) {
343+
if (fgets(buffer.data(), buffer.size(), pipe) != nullptr) {
344+
_pclose(pipe);
345+
return "py.exe -3";
346+
}
347+
_pclose(pipe);
348+
}
349+
350+
return "python.exe"; // fallback
351+
#else
352+
// On Unix-like systems, try python3 first, then python
353+
std::array<char, 1024> buffer;
354+
FILE *pipe = popen("which python3", "r");
355+
if (pipe) {
356+
if (fgets(buffer.data(), buffer.size(), pipe) != nullptr) {
357+
pclose(pipe);
358+
return "python3";
359+
}
360+
pclose(pipe);
361+
}
362+
363+
pipe = popen("which python", "r");
364+
if (pipe) {
365+
if (fgets(buffer.data(), buffer.size(), pipe) != nullptr) {
366+
pclose(pipe);
367+
return "python";
368+
}
369+
pclose(pipe);
370+
}
371+
372+
return "python3"; // fallback
373+
#endif
374+
}
375+
330376
static void init() {
331377
try {
332-
// let's hack and find python paths...
333-
boost::process::ipstream opipe;
334-
boost::process::child cmd("python -c \"import sys; print(sys.path)\"", boost::process::std_out > opipe);
335-
cmd.join();
336-
if (cmd.exit_code() == 0) {
337-
std::stringstream ss;
338-
auto s = opipe.rdbuf();
339-
ss << s;
340-
auto paths_str = ss.str();
341-
std::replace(paths_str.begin(), paths_str.end(), '\'', '\"');
342-
auto jpaths = nlohmann::json::parse(paths_str);
343-
std::vector<std::string> paths = jpaths;
344-
for (auto &path : paths) {
345-
SHLOG_DEBUG("PY PATH: {}", path);
378+
// Let's probe python paths using popen instead of Boost.Process
379+
std::array<char, 4096> buffer; // Increased buffer size
380+
std::string paths_str;
381+
382+
std::string cmd = getPythonCommand() + " -c \"import sys, json; print(json.dumps(sys.path))\"";
383+
384+
#ifdef _WIN32
385+
FILE *pipe = _popen(cmd.c_str(), "r");
386+
#else
387+
FILE *pipe = popen(cmd.c_str(), "r");
388+
#endif
389+
390+
if (pipe) {
391+
while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) {
392+
paths_str += buffer.data();
346393
}
347-
Path = paths;
394+
395+
#ifdef _WIN32
396+
_pclose(pipe);
397+
#else
398+
pclose(pipe);
399+
#endif
400+
401+
if (!paths_str.empty()) {
402+
try {
403+
// No need to replace quotes since we're using json.dumps in Python
404+
auto jpaths = nlohmann::json::parse(paths_str);
405+
std::vector<std::string> paths = jpaths;
406+
for (auto &path : paths) {
407+
SHLOG_DEBUG("PY PATH: {}", path);
408+
}
409+
Path = paths;
410+
} catch (const nlohmann::json::exception &e) {
411+
SHLOG_ERROR("Failed to parse Python paths: {}", e.what());
412+
}
413+
}
414+
} else {
415+
SHLOG_ERROR("Failed to execute Python path probe command: {}", cmd);
348416
}
349417
} catch (const std::exception &ex) {
350418
SHLOG_ERROR("Error while probing python: {}", ex.what());
@@ -353,28 +421,79 @@ struct Env {
353421
static const auto version_patterns = {"3.13", "313", "3.12", "312", "3.11", "311", "3.10", "310",
354422
"3.9", "39", "3.8", "38", "3.7", "37", "3", ""};
355423
std::vector<std::string> candidates;
356-
// prefer this order
424+
425+
// Build base filenames first
426+
std::vector<std::string> base_names;
357427
#ifdef _WIN32
358428
for (auto &pattern : version_patterns) {
359-
candidates.emplace_back(std::string("python") + pattern + ".dll");
429+
base_names.emplace_back(std::string("python") + pattern + ".dll");
360430
}
361431
for (auto &pattern : version_patterns) {
362-
candidates.emplace_back(std::string("libpython") + pattern + ".dll");
363-
candidates.emplace_back(std::string("libpython") + pattern + "m.dll");
432+
base_names.emplace_back(std::string("libpython") + pattern + ".dll");
433+
base_names.emplace_back(std::string("libpython") + pattern + "m.dll");
364434
}
365435
#elif defined(__APPLE__)
366436
for (auto &pattern : version_patterns) {
367-
candidates.emplace_back(std::string("libpython") + pattern + ".dylib");
368-
candidates.emplace_back(std::string("libpython") + pattern + "m.dylib");
437+
base_names.emplace_back(std::string("libpython") + pattern + ".dylib");
438+
base_names.emplace_back(std::string("libpython") + pattern + "m.dylib");
369439
}
370440
#else
371441
for (auto &pattern : version_patterns) {
372-
candidates.emplace_back(std::string("libpython") + pattern + ".so");
373-
candidates.emplace_back(std::string("libpython") + pattern + ".so.1");
374-
candidates.emplace_back(std::string("libpython") + pattern + "m.so");
375-
candidates.emplace_back(std::string("libpython") + pattern + "m.so.1");
442+
base_names.emplace_back(std::string("libpython") + pattern + ".so");
443+
base_names.emplace_back(std::string("libpython") + pattern + ".so.1");
444+
base_names.emplace_back(std::string("libpython") + pattern + "m.so");
445+
base_names.emplace_back(std::string("libpython") + pattern + "m.so.1");
446+
}
447+
#endif
448+
449+
// First add system library paths (empty path for system default locations)
450+
for (const auto &base_name : base_names) {
451+
candidates.push_back(base_name);
376452
}
453+
454+
// Then try all discovered Python paths
455+
for (const auto &path : Path) {
456+
if (path.empty())
457+
continue;
458+
459+
namespace fs = std::filesystem;
460+
fs::path base_path = fs::path(path);
461+
462+
// Try in the python path directly
463+
for (const auto &base_name : base_names) {
464+
candidates.push_back((base_path / base_name).string());
465+
}
466+
467+
// Check if this is a python version specific directory (e.g. python3.12)
468+
std::string dirname = base_path.filename().string();
469+
if (dirname.starts_with("python3")) {
470+
// Also check the parent directory (common in conda/mamba environments)
471+
if (base_path.has_parent_path()) {
472+
fs::path parent_path = base_path.parent_path();
473+
for (const auto &base_name : base_names) {
474+
candidates.push_back((parent_path / base_name).string());
475+
}
476+
}
477+
}
478+
479+
// Also try in lib subdirectory if it exists
480+
fs::path lib_path = base_path / "lib";
481+
if (fs::exists(lib_path) && fs::is_directory(lib_path)) {
482+
for (const auto &base_name : base_names) {
483+
candidates.push_back((lib_path / base_name).string());
484+
}
485+
}
486+
487+
#ifdef __APPLE__
488+
// On macOS, also check in Frameworks directory
489+
fs::path fw_path = base_path / "Frameworks";
490+
if (fs::exists(fw_path) && fs::is_directory(fw_path)) {
491+
for (const auto &base_name : base_names) {
492+
candidates.push_back((fw_path / base_name).string());
493+
}
494+
}
377495
#endif
496+
}
378497

379498
SHLOG_TRACE("Probing python versions: {}", candidates.size());
380499

@@ -516,6 +635,32 @@ struct Env {
516635
assert(result != nullptr);
517636
return result;
518637
}
638+
case SHType::Seq: {
639+
// Create a new Python list
640+
auto size = var.payload.seqValue.len;
641+
PyObject *list = _tupleNew(size);
642+
if (list == nullptr) {
643+
throw SHException("Failed to create Python list for sequence!");
644+
}
645+
646+
// Convert each element in the sequence
647+
size_t idx = 0;
648+
for (const auto &subVar : var) {
649+
PyObject *item = var2Py(subVar);
650+
if (item == nullptr) {
651+
// Clean up on error
652+
list->refcount--;
653+
if (list->refcount == 0 && list->type) {
654+
list->type->dealloc(list);
655+
}
656+
throw SHException("Failed to convert sequence element to Python object!");
657+
}
658+
// PyTuple_SetItem steals the reference, no need to decref item
659+
_tupleSetItem(list, idx++, item);
660+
}
661+
662+
return list;
663+
}
519664
case SHType::Int2: {
520665
PyObject *result = _buildValue("(LL)", var.payload.int2Value[0], var.payload.int2Value[1]);
521666
assert(result != nullptr);
@@ -842,7 +987,7 @@ struct Env {
842987
auto &str = std::get<0>(tstr);
843988
if (str.size() > 3 && str.substr(str.size() - 3, 3) == "Seq") {
844989
auto &inner = innerInfos.emplace_back(SHTypeInfo{Env::toSHType(str.substr(0, str.size() - 3))});
845-
auto seqType = types.emplace_back(SHTypeInfo{SHType::Seq});
990+
auto &seqType = types.emplace_back(SHTypeInfo{SHType::Seq});
846991
seqType.seqTypes = {&inner, 1, 0};
847992
} else {
848993
types.emplace_back(SHTypeInfo{Env::toSHType(str)});

0 commit comments

Comments
 (0)