2828import java .security .MessageDigest ;
2929import java .security .NoSuchAlgorithmException ;
3030import java .util .Base64 ;
31+ import java .util .Collections ;
3132import java .util .Comparator ;
3233import java .util .LinkedHashMap ;
3334import java .util .Map ;
@@ -76,6 +77,18 @@ protected boolean removeEldestEntry(Map.Entry<String, Path> eldest) {
7677 * @return Path to the workspace directory containing .venv
7778 */
7879 static Path getOrCreateWorkspace (String pyprojectContent ) {
80+ return getOrCreateWorkspace (pyprojectContent , Collections .emptyMap ());
81+ }
82+
83+ /**
84+ * Gets or creates a workspace directory for the given pyproject.toml content.
85+ * Workspaces are cached by content hash to avoid repeated installations.
86+ *
87+ * @param pyprojectContent The complete pyproject.toml file content
88+ * @param environment additional environment variables for subprocess (e.g., SSL_CERT_FILE)
89+ * @return Path to the workspace directory containing .venv
90+ */
91+ static Path getOrCreateWorkspace (String pyprojectContent , Map <String , String > environment ) {
7992 String hash = hashContent (pyprojectContent );
8093
8194 // Check in-memory cache
@@ -106,10 +119,10 @@ static Path getOrCreateWorkspace(String pyprojectContent) {
106119 );
107120
108121 // Sync: creates .venv, generates uv.lock, and installs dependencies
109- runCommand (tempDir , "uv" , "sync" );
122+ runCommand (tempDir , environment , "uv" , "sync" );
110123
111124 // Install ty for type stubs
112- runCommand (tempDir , "uv" , "pip" , "install" , "ty" );
125+ runCommand (tempDir , environment , "uv" , "pip" , "install" , "ty" );
113126
114127 // Write workspace version for cache invalidation
115128 Files .write (tempDir .resolve ("version.txt" ),
@@ -152,6 +165,21 @@ static Path getOrCreateWorkspace(String pyprojectContent) {
152165 */
153166 static @ Nullable Path getOrCreateRequirementsWorkspace (String requirementsContent ,
154167 @ Nullable Path originalFilePath ) {
168+ return getOrCreateRequirementsWorkspace (requirementsContent , originalFilePath , Collections .emptyMap ());
169+ }
170+
171+ /**
172+ * Gets or creates a workspace directory for a requirements.txt file.
173+ * Returns null (graceful degradation) when uv is unavailable.
174+ *
175+ * @param requirementsContent The complete requirements.txt content
176+ * @param originalFilePath The original file path on disk (supports -r includes), or null
177+ * @param environment additional environment variables for subprocess (e.g., SSL_CERT_FILE)
178+ * @return Path to the workspace directory, or null if uv is unavailable
179+ */
180+ static @ Nullable Path getOrCreateRequirementsWorkspace (String requirementsContent ,
181+ @ Nullable Path originalFilePath ,
182+ Map <String , String > environment ) {
155183 String uvPath = UvExecutor .findUvExecutable ();
156184 if (uvPath == null ) {
157185 return null ;
@@ -180,7 +208,7 @@ static Path getOrCreateWorkspace(String pyprojectContent) {
180208
181209 try {
182210 // Create virtualenv
183- runCommandWithPath (tempDir , uvPath , "venv" );
211+ runCommandWithPath (tempDir , uvPath , environment , "venv" );
184212
185213 // Install dependencies from requirements file
186214 Path reqFile ;
@@ -190,10 +218,10 @@ static Path getOrCreateWorkspace(String pyprojectContent) {
190218 reqFile = tempDir .resolve ("requirements.txt" );
191219 Files .write (reqFile , requirementsContent .getBytes (StandardCharsets .UTF_8 ));
192220 }
193- runCommandWithPath (tempDir , uvPath , "pip" , "install" , "-r" , reqFile .toString ());
221+ runCommandWithPath (tempDir , uvPath , environment , "pip" , "install" , "-r" , reqFile .toString ());
194222
195223 // Capture freeze output BEFORE installing ty
196- UvExecutor .RunResult freezeResult = UvExecutor .run (tempDir , uvPath , "pip" , "freeze" );
224+ UvExecutor .RunResult freezeResult = UvExecutor .run (tempDir , uvPath , environment , "pip" , "freeze" );
197225 if (freezeResult .isSuccess ()) {
198226 Files .write (
199227 tempDir .resolve ("freeze.txt" ),
@@ -202,7 +230,7 @@ static Path getOrCreateWorkspace(String pyprojectContent) {
202230 }
203231
204232 // Install ty for type stubs (after freeze so it's not in the dep model)
205- runCommandWithPath (tempDir , uvPath , "pip" , "install" , "ty" );
233+ runCommandWithPath (tempDir , uvPath , environment , "pip" , "install" , "ty" );
206234
207235 // Write workspace version for cache invalidation
208236 Files .write (tempDir .resolve ("version.txt" ),
@@ -243,6 +271,22 @@ static Path getOrCreateWorkspace(String pyprojectContent) {
243271 */
244272 public static @ Nullable Path getOrCreateSetuptoolsWorkspace (String manifestContent ,
245273 @ Nullable Path projectDir ) {
274+ return getOrCreateSetuptoolsWorkspace (manifestContent , projectDir , Collections .emptyMap ());
275+ }
276+
277+ /**
278+ * Gets or creates a workspace directory for a setuptools project (setup.cfg / setup.py).
279+ * Uses {@code uv pip install <projectDir>} to install the project and its dependencies.
280+ * Returns null (graceful degradation) when uv is unavailable.
281+ *
282+ * @param manifestContent The setup.cfg (or setup.py) content for hashing
283+ * @param projectDir The project directory to install from, or null
284+ * @param environment additional environment variables for subprocess (e.g., SSL_CERT_FILE)
285+ * @return Path to the workspace directory, or null if uv is unavailable
286+ */
287+ public static @ Nullable Path getOrCreateSetuptoolsWorkspace (String manifestContent ,
288+ @ Nullable Path projectDir ,
289+ Map <String , String > environment ) {
246290 String uvPath = UvExecutor .findUvExecutable ();
247291 if (uvPath == null ) {
248292 return null ;
@@ -275,13 +319,13 @@ static Path getOrCreateWorkspace(String pyprojectContent) {
275319
276320 try {
277321 // Create virtualenv
278- runCommandWithPath (tempDir , uvPath , "venv" );
322+ runCommandWithPath (tempDir , uvPath , environment , "venv" );
279323
280324 // Install from the project directory
281- runCommandWithPath (tempDir , uvPath , "pip" , "install" , projectDir .toString ());
325+ runCommandWithPath (tempDir , uvPath , environment , "pip" , "install" , projectDir .toString ());
282326
283327 // Capture freeze output BEFORE installing ty
284- UvExecutor .RunResult freezeResult = UvExecutor .run (tempDir , uvPath , "pip" , "freeze" );
328+ UvExecutor .RunResult freezeResult = UvExecutor .run (tempDir , uvPath , environment , "pip" , "freeze" );
285329 if (freezeResult .isSuccess ()) {
286330 Files .write (
287331 tempDir .resolve ("freeze.txt" ),
@@ -290,7 +334,7 @@ static Path getOrCreateWorkspace(String pyprojectContent) {
290334 }
291335
292336 // Install ty for type stubs (after freeze so it's not in the dep model)
293- runCommandWithPath (tempDir , uvPath , "pip" , "install" , "ty" );
337+ runCommandWithPath (tempDir , uvPath , environment , "pip" , "install" , "ty" );
294338
295339 // Write workspace version for cache invalidation
296340 Files .write (tempDir .resolve ("version.txt" ),
@@ -335,14 +379,14 @@ private static boolean isRequirementsWorkspaceValid(Path workspaceDir) {
335379 hasCurrentVersion (workspaceDir );
336380 }
337381
338- private static void runCommandWithPath (Path dir , String uvPath , String ... args ) throws IOException , InterruptedException {
339- UvExecutor .RunResult result = UvExecutor .run (dir , uvPath , args );
382+ private static void runCommandWithPath (Path dir , String uvPath , Map < String , String > environment , String ... args ) throws IOException , InterruptedException {
383+ UvExecutor .RunResult result = UvExecutor .run (dir , uvPath , environment , args );
340384 if (!result .isSuccess ()) {
341385 throw new RuntimeException ("uv " + String .join (" " , args ) + " failed with exit code: " + result .getExitCode ());
342386 }
343387 }
344388
345- private static void runCommand (Path dir , String ... command ) throws IOException , InterruptedException {
389+ private static void runCommand (Path dir , Map < String , String > environment , String ... command ) throws IOException , InterruptedException {
346390 String uvPath = UvExecutor .findUvExecutable ();
347391 if (uvPath == null ) {
348392 throw new RuntimeException ("uv is not installed. Install it with: pip install uv" );
@@ -352,7 +396,7 @@ private static void runCommand(Path dir, String... command) throws IOException,
352396 String [] args = new String [command .length - 1 ];
353397 System .arraycopy (command , 1 , args , 0 , args .length );
354398
355- UvExecutor .RunResult result = UvExecutor .run (dir , uvPath , args );
399+ UvExecutor .RunResult result = UvExecutor .run (dir , uvPath , environment , args );
356400 if (!result .isSuccess ()) {
357401 throw new RuntimeException (String .join (" " , command ) + " failed with exit code: " + result .getExitCode ());
358402 }
0 commit comments