Skip to content

Commit e78d1e6

Browse files
committed
Propagate SSL/proxy environment to pip and uv subprocesses
The environment map configured via PythonRewriteRpc.builder().environment() was applied to the RPC server process but not to the pip bootstrap subprocess (bootstrapOpenrewrite) or uv subprocesses (via UvExecutor). This caused pip install and uv commands to fail behind corporate proxies with SSL inspection, since they never received SSL_CERT_FILE, PIP_CERT, or proxy configuration. - Apply environment to the pip ProcessBuilder in bootstrapOpenrewrite() - Add environment parameter to UvExecutor.run() and thread it through DependencyWorkspace, UvLockRegeneration, SetupCfgParser, and RequirementsTxtParser via explicit method/constructor parameters - In PythonRewriteRpc, pass commandEnv to parseManifest() parsers and createSetupPyMarker() so all downstream subprocess calls inherit SSL/proxy configuration - Backward-compatible overloads with empty maps for all public APIs
1 parent 20efcb1 commit e78d1e6

File tree

6 files changed

+120
-22
lines changed

6 files changed

+120
-22
lines changed

rewrite-python/src/main/java/org/openrewrite/python/DependencyWorkspace.java

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.security.MessageDigest;
2929
import java.security.NoSuchAlgorithmException;
3030
import java.util.Base64;
31+
import java.util.Collections;
3132
import java.util.Comparator;
3233
import java.util.LinkedHashMap;
3334
import 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
}

rewrite-python/src/main/java/org/openrewrite/python/RequirementsTxtParser.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
import java.util.regex.Pattern;
3636
import java.util.stream.Stream;
3737

38+
import static java.util.Collections.emptyMap;
39+
3840
import static org.openrewrite.Tree.randomId;
3941

4042
/**
@@ -50,6 +52,15 @@ public class RequirementsTxtParser implements Parser {
5052
private static final Pattern EXTRA_MARKER_PATTERN = Pattern.compile("\\bextra\\s*==");
5153

5254
private final PlainTextParser plainTextParser = new PlainTextParser();
55+
private final Map<String, String> subprocessEnvironment;
56+
57+
public RequirementsTxtParser() {
58+
this(emptyMap());
59+
}
60+
61+
public RequirementsTxtParser(Map<String, String> subprocessEnvironment) {
62+
this.subprocessEnvironment = subprocessEnvironment;
63+
}
5364

5465
@Override
5566
public Stream<SourceFile> parseInputs(Iterable<Input> sources, @Nullable Path relativeTo, ExecutionContext ctx) {
@@ -64,7 +75,7 @@ public Stream<SourceFile> parseInputs(Iterable<Input> sources, @Nullable Path re
6475
originalFilePath = relativeTo.resolve(text.getSourcePath());
6576
}
6677
Path workspace = DependencyWorkspace.getOrCreateRequirementsWorkspace(
67-
text.getText(), originalFilePath);
78+
text.getText(), originalFilePath, subprocessEnvironment);
6879
if (workspace == null) {
6980
return sf;
7081
}

rewrite-python/src/main/java/org/openrewrite/python/SetupCfgParser.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.nio.file.Path;
3030
import java.util.Collections;
3131
import java.util.List;
32+
import java.util.Map;
3233
import java.util.stream.Stream;
3334

3435
import static org.openrewrite.Tree.randomId;
@@ -41,6 +42,15 @@
4142
public class SetupCfgParser implements Parser {
4243

4344
private final PlainTextParser plainTextParser = new PlainTextParser();
45+
private final Map<String, String> subprocessEnvironment;
46+
47+
public SetupCfgParser() {
48+
this(Collections.emptyMap());
49+
}
50+
51+
public SetupCfgParser(Map<String, String> subprocessEnvironment) {
52+
this.subprocessEnvironment = subprocessEnvironment;
53+
}
4454

4555
@Override
4656
public Stream<SourceFile> parseInputs(Iterable<Input> sources, @Nullable Path relativeTo, ExecutionContext ctx) {
@@ -56,7 +66,7 @@ public Stream<SourceFile> parseInputs(Iterable<Input> sources, @Nullable Path re
5666
projectDir = filePath.getParent();
5767
}
5868
Path workspace = DependencyWorkspace.getOrCreateSetuptoolsWorkspace(
59-
text.getText(), projectDir);
69+
text.getText(), projectDir, subprocessEnvironment);
6070
if (workspace == null) {
6171
return sf;
6272
}

rewrite-python/src/main/java/org/openrewrite/python/internal/UvExecutor.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
import java.nio.file.Path;
2727
import java.nio.file.Paths;
2828
import java.util.ArrayList;
29+
import java.util.Collections;
2930
import java.util.List;
31+
import java.util.Map;
3032
import java.util.concurrent.TimeUnit;
3133

3234
/**
@@ -37,7 +39,6 @@ public class UvExecutor {
3739

3840
private static final long DEFAULT_TIMEOUT_SECONDS = 120;
3941
private static @Nullable String cachedUvPath;
40-
4142
@Value
4243
public static class RunResult {
4344
boolean success;
@@ -55,12 +56,26 @@ public static class RunResult {
5556
* @return the run result
5657
*/
5758
public static RunResult run(Path workDir, String uvPath, String... args) throws IOException, InterruptedException {
59+
return run(workDir, uvPath, Collections.emptyMap(), args);
60+
}
61+
62+
/**
63+
* Run a uv command in the given directory with additional environment variables.
64+
*
65+
* @param workDir the working directory
66+
* @param uvPath the path to the uv executable
67+
* @param environment additional environment variables (e.g., SSL_CERT_FILE, HTTP_PROXY)
68+
* @param args the arguments to pass to uv
69+
* @return the run result
70+
*/
71+
public static RunResult run(Path workDir, String uvPath, Map<String, String> environment, String... args) throws IOException, InterruptedException {
5872
String[] command = new String[args.length + 1];
5973
command[0] = uvPath;
6074
System.arraycopy(args, 0, command, 1, args.length);
6175

6276
ProcessBuilder pb = new ProcessBuilder(command);
6377
pb.directory(workDir.toFile());
78+
pb.environment().putAll(environment);
6479
pb.redirectErrorStream(false);
6580

6681
Process process = pb.start();

rewrite-python/src/main/java/org/openrewrite/python/internal/UvLockRegeneration.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
import java.nio.charset.StandardCharsets;
2424
import java.nio.file.Files;
2525
import java.nio.file.Path;
26+
import java.util.Collections;
2627
import java.util.Comparator;
28+
import java.util.Map;
2729

2830
/**
2931
* Utility for regenerating uv.lock files by running {@code uv lock} in a temporary directory.
@@ -53,7 +55,7 @@ public static Result failure(String errorMessage) {
5355
* @return a result containing the new lock file content, or an error message
5456
*/
5557
public static Result regenerate(String pyprojectContent) {
56-
return regenerate(pyprojectContent, null);
58+
return regenerate(pyprojectContent, null, Collections.emptyMap());
5759
}
5860

5961
/**
@@ -67,6 +69,21 @@ public static Result regenerate(String pyprojectContent) {
6769
* @return a result containing the new lock file content, or an error message
6870
*/
6971
public static Result regenerate(String pyprojectContent, @Nullable String existingLockContent) {
72+
return regenerate(pyprojectContent, existingLockContent, Collections.emptyMap());
73+
}
74+
75+
/**
76+
* Regenerate a uv.lock file from the given pyproject.toml content.
77+
* When an existing lock file is provided it is seeded into the working
78+
* directory so that {@code uv lock} performs a minimal update rather
79+
* than re-resolving every dependency from scratch.
80+
*
81+
* @param pyprojectContent the pyproject.toml content to lock
82+
* @param existingLockContent the current uv.lock content, or {@code null}
83+
* @param environment additional environment variables for subprocess (e.g., SSL_CERT_FILE)
84+
* @return a result containing the new lock file content, or an error message
85+
*/
86+
public static Result regenerate(String pyprojectContent, @Nullable String existingLockContent, Map<String, String> environment) {
7087
String uvPath = UvExecutor.findUvExecutable();
7188
if (uvPath == null) {
7289
return Result.failure("uv is not installed. Install it with: pip install uv");
@@ -88,7 +105,7 @@ public static Result regenerate(String pyprojectContent, @Nullable String existi
88105
);
89106
}
90107

91-
UvExecutor.RunResult runResult = UvExecutor.run(tempDir, uvPath, "lock");
108+
UvExecutor.RunResult runResult = UvExecutor.run(tempDir, uvPath, environment, "lock");
92109
if (!runResult.isSuccess()) {
93110
return Result.failure("uv lock failed (exit code " + runResult.getExitCode() + "): " + runResult.getStderr());
94111
}

rewrite-python/src/main/java/org/openrewrite/python/rpc/PythonRewriteRpc.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ public int characteristics() {
273273
return null;
274274
}
275275

276-
Path workspace = DependencyWorkspace.getOrCreateSetuptoolsWorkspace(source, projectPath);
276+
Path workspace = DependencyWorkspace.getOrCreateSetuptoolsWorkspace(source, projectPath, commandEnv);
277277
if (workspace == null) {
278278
return null;
279279
}
@@ -334,11 +334,11 @@ private Stream<SourceFile> parseManifest(Path projectPath, @Nullable Path relati
334334
Path setupCfgPath = projectPath.resolve("setup.cfg");
335335
if (Files.exists(setupCfgPath)) {
336336
Parser.Input input = Parser.Input.fromFile(setupCfgPath);
337-
return new SetupCfgParser().parseInputs(
337+
return new SetupCfgParser(commandEnv).parseInputs(
338338
Collections.singletonList(input), effectiveRelativeTo, ctx);
339339
}
340340

341-
RequirementsTxtParser reqsParser = new RequirementsTxtParser();
341+
RequirementsTxtParser reqsParser = new RequirementsTxtParser(commandEnv);
342342
try (Stream<Path> entries = Files.list(projectPath)) {
343343
Path reqsPath = entries
344344
.filter(p -> reqsParser.accept(p.getFileName()))
@@ -718,6 +718,7 @@ private void bootstrapOpenrewrite(Path pipPackagesPath, String version) {
718718
"--target=" + pipPackagesPath.toAbsolutePath().normalize(),
719719
"openrewrite==" + version
720720
);
721+
pb.environment().putAll(environment);
721722
pb.redirectErrorStream(true);
722723
if (log != null) {
723724
File logFile = log.toAbsolutePath().normalize().toFile();

0 commit comments

Comments
 (0)