Skip to content

Commit 16af78a

Browse files
committed
Add virtual environment detection for Python paths
- Implement a helper function to detect Python virtual environments by searching for `pyvenv.cfg` files. - Enhance path resolution by adding home and base-prefix paths from the virtual environment configuration. - Prioritize virtual environment paths when resolving Python libraries and executables. - Improve logging for detected virtual environment configurations.
1 parent 3a00642 commit 16af78a

File tree

1 file changed

+158
-0
lines changed

1 file changed

+158
-0
lines changed

shards/modules/py/py.cpp

+158
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
#include <filesystem>
1111
#include <nlohmann/json.hpp>
12+
#include <fstream> // Include for std::ifstream
1213

1314
#if defined(__linux__) || defined(__APPLE__)
1415
#include <dlfcn.h>
@@ -325,6 +326,127 @@ struct Env {
325326

326327
static inline std::vector<std::string> Path;
327328

329+
// Helper function to detect virtual environments and find base Python paths
330+
static std::vector<std::string> detectVenvBasePaths() {
331+
std::vector<std::string> venvBasePaths;
332+
333+
// First, check if we're running from a virtual environment
334+
// by checking for pyvenv.cfg in any of the paths
335+
for (const auto &path : Path) {
336+
if (path.empty())
337+
continue;
338+
339+
namespace fs = std::filesystem;
340+
fs::path base_path = fs::path(path);
341+
342+
// Check if this might be inside a venv by looking for the parent directories
343+
fs::path venv_root;
344+
fs::path check_path = base_path;
345+
bool found_venv = false;
346+
347+
// Look up the directory tree for a pyvenv.cfg file (up to 3 levels)
348+
for (int i = 0; i < 3 && !found_venv; i++) {
349+
fs::path cfg_path = check_path / "pyvenv.cfg";
350+
if (fs::exists(cfg_path)) {
351+
venv_root = check_path;
352+
found_venv = true;
353+
break;
354+
}
355+
356+
// Also check one level up (common structure: venv/lib/pythonX.Y/site-packages)
357+
if (check_path.has_parent_path()) {
358+
check_path = check_path.parent_path();
359+
cfg_path = check_path / "pyvenv.cfg";
360+
if (fs::exists(cfg_path)) {
361+
venv_root = check_path;
362+
found_venv = true;
363+
break;
364+
}
365+
} else {
366+
break;
367+
}
368+
}
369+
370+
if (found_venv) {
371+
// Found a venv, parse the pyvenv.cfg file to find the base Python
372+
fs::path cfg_path = venv_root / "pyvenv.cfg";
373+
SHLOG_DEBUG("Found Python venv config: {}", cfg_path.string());
374+
375+
std::ifstream cfg_file(cfg_path.string());
376+
if (cfg_file.is_open()) {
377+
std::string line;
378+
std::string home_path;
379+
std::string base_prefix;
380+
381+
while (std::getline(cfg_file, line)) {
382+
// Parse key = value format
383+
auto pos = line.find('=');
384+
if (pos != std::string::npos) {
385+
std::string key = line.substr(0, pos);
386+
std::string value = line.substr(pos + 1);
387+
388+
// Trim whitespace
389+
key.erase(0, key.find_first_not_of(" \t"));
390+
key.erase(key.find_last_not_of(" \t") + 1);
391+
value.erase(0, value.find_first_not_of(" \t"));
392+
value.erase(value.find_last_not_of(" \t") + 1);
393+
394+
if (key == "home") {
395+
home_path = value;
396+
} else if (key == "base-prefix") {
397+
base_prefix = value;
398+
}
399+
}
400+
}
401+
402+
// Add the home path if found
403+
if (!home_path.empty()) {
404+
fs::path home(home_path);
405+
venvBasePaths.push_back(home.string());
406+
SHLOG_DEBUG("Found Python venv home path: {}", home.string());
407+
408+
// Also add common library locations relative to home
409+
fs::path lib_path = home / "lib";
410+
if (fs::exists(lib_path) && fs::is_directory(lib_path)) {
411+
venvBasePaths.push_back(lib_path.string());
412+
}
413+
414+
#ifdef __APPLE__
415+
// On macOS, also check in Frameworks directory
416+
fs::path fw_path = home / "Frameworks";
417+
if (fs::exists(fw_path) && fs::is_directory(fw_path)) {
418+
venvBasePaths.push_back(fw_path.string());
419+
}
420+
#endif
421+
}
422+
423+
// Add base-prefix if found and different from home
424+
if (!base_prefix.empty() && base_prefix != home_path) {
425+
fs::path base(base_prefix);
426+
venvBasePaths.push_back(base.string());
427+
SHLOG_DEBUG("Found Python venv base-prefix: {}", base.string());
428+
429+
// Also add common library locations relative to base prefix
430+
fs::path lib_path = base / "lib";
431+
if (fs::exists(lib_path) && fs::is_directory(lib_path)) {
432+
venvBasePaths.push_back(lib_path.string());
433+
}
434+
435+
#ifdef __APPLE__
436+
// On macOS, also check in Frameworks directory
437+
fs::path fw_path = base / "Frameworks";
438+
if (fs::exists(fw_path) && fs::is_directory(fw_path)) {
439+
venvBasePaths.push_back(fw_path.string());
440+
}
441+
#endif
442+
}
443+
}
444+
}
445+
}
446+
447+
return venvBasePaths;
448+
}
449+
328450
static std::string getPythonCommand() {
329451
#ifdef _WIN32
330452
// On Windows, try python.exe first, then py.exe (Python launcher)
@@ -414,6 +536,12 @@ struct Env {
414536
} else {
415537
SHLOG_ERROR("Failed to execute Python path probe command: {}", cmd);
416538
}
539+
540+
// Get extra paths from venv configuration
541+
std::vector<std::string> venvBasePaths = detectVenvBasePaths();
542+
for (const auto &path : venvBasePaths) {
543+
SHLOG_DEBUG("PY VENV BASE PATH: {}", path);
544+
}
417545
} catch (const std::exception &ex) {
418546
SHLOG_ERROR("Error while probing python: {}", ex.what());
419547
}
@@ -451,6 +579,36 @@ struct Env {
451579
candidates.push_back(base_name);
452580
}
453581

582+
// Check venv paths first (priority)
583+
std::vector<std::string> venvBasePaths = detectVenvBasePaths();
584+
for (const auto &path : venvBasePaths) {
585+
namespace fs = std::filesystem;
586+
fs::path base_path = fs::path(path);
587+
588+
// Try in the path directly
589+
for (const auto &base_name : base_names) {
590+
candidates.push_back((base_path / base_name).string());
591+
}
592+
593+
// Also try in lib subdirectory if it exists
594+
fs::path lib_path = base_path / "lib";
595+
if (fs::exists(lib_path) && fs::is_directory(lib_path)) {
596+
for (const auto &base_name : base_names) {
597+
candidates.push_back((lib_path / base_name).string());
598+
}
599+
}
600+
601+
#ifdef __APPLE__
602+
// On macOS, also check in Frameworks directory
603+
fs::path fw_path = base_path / "Frameworks";
604+
if (fs::exists(fw_path) && fs::is_directory(fw_path)) {
605+
for (const auto &base_name : base_names) {
606+
candidates.push_back((fw_path / base_name).string());
607+
}
608+
}
609+
#endif
610+
}
611+
454612
// Then try all discovered Python paths
455613
for (const auto &path : Path) {
456614
if (path.empty())

0 commit comments

Comments
 (0)