Skip to content

Commit 70d5bb9

Browse files
committed
Fix CI: add venv site-packages to PYTHONPATH for msgpack import
When a custom pythonExecutable (e.g. 'python3') is used with a venv, the venv's site-packages are not on sys.path, causing import msgpack to fail. This commit adds indVenvSitePackages() which locates lib/python3.X/site-packages (Unix) or Lib/site-packages (Windows) inside the venv, and appends it to PYTHONPATH before starting the Python process. Added 4 unit tests for the new helper method.
1 parent 663d323 commit 70d5bb9

2 files changed

Lines changed: 117 additions & 2 deletions

File tree

python-embed-runtime/src/main/java/io/github/howtis/pythonembed/PythonProcessManager.java

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.io.InputStreamReader;
77
import java.io.OutputStream;
88
import java.nio.charset.StandardCharsets;
9+
import java.nio.file.Files;
910
import java.nio.file.Path;
1011
import java.util.Collections;
1112
import java.util.Map;
@@ -14,6 +15,7 @@
1415
import java.util.concurrent.TimeUnit;
1516
import java.util.logging.Level;
1617
import java.util.logging.Logger;
18+
import java.util.stream.Stream;
1719

1820
/**
1921
* Manages a persistent CPython REPL process that communicates via stdin/stdout
@@ -76,9 +78,21 @@ void start(Path venvPath, Path bridgeScriptPath, Map<String, String> env,
7678
+ " --max-code-length " + maxCodeLength);
7779
pb.redirectErrorStream(false);
7880

79-
// Apply environment variables
81+
// Add venv site-packages to PYTHONPATH so msgpack is importable
82+
Map<String, String> pbEnv = pb.environment();
83+
if (venvPath != null) {
84+
Path sitePackages = findVenvSitePackages(venvPath);
85+
if (sitePackages != null) {
86+
String existing = pbEnv.getOrDefault("PYTHONPATH", "");
87+
String newPath = sitePackages.toAbsolutePath().toString();
88+
String pythonPath = existing.isEmpty() ? newPath : newPath + java.io.File.pathSeparator + existing;
89+
pbEnv.put("PYTHONPATH", pythonPath);
90+
logger.info(() -> "Python process PYTHONPATH: " + pythonPath);
91+
}
92+
}
93+
94+
// Apply environment variables (may override PYTHONPATH)
8095
if (env != null && !env.isEmpty()) {
81-
Map<String, String> pbEnv = pb.environment();
8296
pbEnv.putAll(env);
8397
logger.info(() -> "Python process env vars: " + env.keySet());
8498
}
@@ -299,6 +313,43 @@ private void shutdownExecutor(ExecutorService executor) {
299313
}
300314

301315

316+
/**
317+
* Finds the site-packages directory inside a venv.
318+
*
319+
* <p>On Unix: {@code lib/python3.X/site-packages}
320+
* On Windows: {@code Lib/site-packages}
321+
*
322+
* @param venvPath root directory of the venv
323+
* @return path to site-packages, or null if not found
324+
*/
325+
static Path findVenvSitePackages(Path venvPath) {
326+
boolean isWindows = System.getProperty("os.name", "").toLowerCase().contains("win");
327+
if (isWindows) {
328+
Path sitePkgs = venvPath.resolve("Lib").resolve("site-packages");
329+
if (Files.isDirectory(sitePkgs)) {
330+
return sitePkgs;
331+
}
332+
return null;
333+
} else {
334+
Path libDir = venvPath.resolve("lib");
335+
if (!Files.isDirectory(libDir)) {
336+
return null;
337+
}
338+
try (Stream<Path> entries = Files.list(libDir)) {
339+
return entries
340+
.filter(Files::isDirectory)
341+
.filter(p -> p.getFileName().toString().startsWith("python"))
342+
.map(p -> p.resolve("site-packages"))
343+
.filter(Files::isDirectory)
344+
.findFirst()
345+
.orElse(null);
346+
} catch (IOException e) {
347+
logger.log(Level.WARNING, "Failed to scan venv lib directory", e);
348+
return null;
349+
}
350+
}
351+
}
352+
302353
/**
303354
* Resolves the Python executable within a directory.
304355
*

python-embed-runtime/src/test/java/io/github/howtis/pythonembed/PythonProcessManagerTest.java

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,70 @@ void resolvePythonExecutable_emptyBasePath() {
164164
assertFalse(resolved.isEmpty());
165165
}
166166

167+
// ------------------------------------------------------------------
168+
// findVenvSitePackages()
169+
// ------------------------------------------------------------------
170+
171+
@Test
172+
void findVenvSitePackages_unixVenvLayout() throws Exception {
173+
tempDir = Files.createTempDirectory("test-venv-sp-");
174+
Path sitePkgs = Files.createDirectories(
175+
tempDir.resolve("lib").resolve("python3.12").resolve("site-packages"));
176+
177+
// On Windows the method looks for Lib/site-packages
178+
boolean isWindows = System.getProperty("os.name", "").toLowerCase().contains("win");
179+
if (isWindows) {
180+
// Windows: set up Lib/site-packages
181+
Path winSitePkgs = Files.createDirectories(
182+
tempDir.resolve("Lib").resolve("site-packages"));
183+
Path found = PythonProcessManager.findVenvSitePackages(tempDir);
184+
assertEquals(winSitePkgs, found);
185+
} else {
186+
Path found = PythonProcessManager.findVenvSitePackages(tempDir);
187+
assertEquals(sitePkgs, found);
188+
}
189+
}
190+
191+
@Test
192+
void findVenvSitePackages_picksFirstMatchingPython() throws Exception {
193+
tempDir = Files.createTempDirectory("test-venv-sp-multi-");
194+
Path first = Files.createDirectories(
195+
tempDir.resolve("lib").resolve("python3.12").resolve("site-packages"));
196+
Path second = Files.createDirectories(
197+
tempDir.resolve("lib").resolve("python3.11").resolve("site-packages"));
198+
199+
Path found = PythonProcessManager.findVenvSitePackages(tempDir);
200+
boolean isWindows = System.getProperty("os.name", "").toLowerCase().contains("win");
201+
if (!isWindows) {
202+
assertNotNull(found);
203+
// Either is acceptable (order depends on Files.list)
204+
assertTrue(found.equals(first) || found.equals(second),
205+
"Expected one of the python site-packages dirs, got: " + found);
206+
}
207+
// On Windows the unix lib dir won't match, returns null; that's fine
208+
}
209+
210+
@Test
211+
void findVenvSitePackages_noLibDir_returnsNull() {
212+
tempDir = new java.io.File("nonexistent-" + System.nanoTime()).toPath();
213+
Path found = PythonProcessManager.findVenvSitePackages(tempDir);
214+
assertNull(found);
215+
}
216+
217+
@Test
218+
void findVenvSitePackages_noSitePackages_returnsNull() throws Exception {
219+
tempDir = Files.createTempDirectory("test-venv-sp-nosp-");
220+
Files.createDirectories(tempDir.resolve("lib").resolve("python3.12"));
221+
// No site-packages subdirectory
222+
223+
Path found = PythonProcessManager.findVenvSitePackages(tempDir);
224+
boolean isWindows = System.getProperty("os.name", "").toLowerCase().contains("win");
225+
if (!isWindows) {
226+
assertNull(found);
227+
}
228+
// On Windows the unix lib dir won't match, returns null; that's fine
229+
}
230+
167231
// ------------------------------------------------------------------
168232
// isRunning()
169233
// ------------------------------------------------------------------

0 commit comments

Comments
 (0)