Skip to content

Commit 746fe6e

Browse files
committed
Python: Unify dev and release bootstrap for openrewrite package
The bootstrap logic now works the same for dev pre-releases (e.g., 8.75.0.dev0) and stable releases: 1. If pipPackagesPath is set, check for a version-specific install there 2. If the interpreter already has the right version, use it as-is 3. Otherwise install via pip to pipPackagesPath (or fail if not set) This eliminates the stale `dev/` directory that previously installed an unpinned PyPI release and never updated it. Dev pre-releases are now installed to version-specific subdirectories like stable releases. For local development without a version file (version is empty), the interpreter must already have the rewrite package available (e.g., via `uv sync --extra dev` in rewrite-python/rewrite/).
1 parent f93dd01 commit 746fe6e

1 file changed

Lines changed: 75 additions & 49 deletions

File tree

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

Lines changed: 75 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -491,10 +491,10 @@ public Builder workingDirectory(@Nullable Path workingDirectory) {
491491

492492
/**
493493
* Set the base pip packages directory.
494-
* When set, a version-specific subdirectory (e.g., {@code <pipPackagesPath>/0.5.3/}
495-
* for releases, or {@code <pipPackagesPath>/dev/} for dev builds) will be resolved
496-
* and added to PYTHONPATH. The openrewrite package is automatically installed
497-
* into that subdirectory if not already present.
494+
* When set and the required version is not already available in the Python
495+
* interpreter, a version-specific subdirectory (e.g., {@code <pipPackagesPath>/8.74.1/}
496+
* or {@code <pipPackagesPath>/8.75.0.dev0/}) is resolved and the openrewrite
497+
* package is automatically installed there via pip.
498498
*
499499
* @param pipPackagesPath The base directory under which version-specific pip packages are installed
500500
* @return This builder
@@ -536,23 +536,38 @@ public Builder pythonVersion(String pythonVersion) {
536536

537537
@Override
538538
public PythonRewriteRpc get() {
539-
// For dev builds (version ending in .dev0), check whether the interpreter
540-
// already has the rewrite package (e.g., from a venv with an editable install).
541-
// If so, skip bootstrap and PYTHONPATH prepend so the interpreter's own
542-
// version takes precedence. For release/CI builds, always use pipPackagesPath
543-
// to ensure the correct pinned version.
544539
String version = StringUtils.readFully(
545540
PythonRewriteRpc.class.getResourceAsStream("/META-INF/rewrite-python-version.txt")).trim();
546-
boolean isDevBuild = version.isEmpty() || version.endsWith(".dev0");
547-
boolean interpreterHasRewrite = isDevBuild && pipPackagesPath != null && canImportRewrite(pythonPath);
548-
boolean usePipPackagesPath = pipPackagesPath != null && !interpreterHasRewrite;
549541

550-
// Resolve version-specific subdirectory under pipPackagesPath
551542
Path resolvedPipPackagesPath = null;
552-
if (usePipPackagesPath) {
553-
String versionDir = isDevBuild ? "dev" : version;
554-
resolvedPipPackagesPath = pipPackagesPath.resolve(versionDir);
555-
bootstrapOpenrewrite(resolvedPipPackagesPath, version, isDevBuild);
543+
if (!version.isEmpty()) {
544+
// Known version (release or dev pre-release) — try to find or install it.
545+
// 1. Check pipPackagesPath for an existing install
546+
// 2. Check if the interpreter already has the right version
547+
// 3. Install to pipPackagesPath if available, otherwise fail
548+
if (pipPackagesPath != null && isVersionInstalled(pipPackagesPath.resolve(version), version)) {
549+
resolvedPipPackagesPath = pipPackagesPath.resolve(version);
550+
} else if (hasRewriteVersion(pythonPath, version)) {
551+
// Interpreter already has the right version; nothing to do
552+
} else if (pipPackagesPath != null) {
553+
resolvedPipPackagesPath = pipPackagesPath.resolve(version);
554+
bootstrapOpenrewrite(resolvedPipPackagesPath, version);
555+
} else {
556+
throw new IllegalStateException(
557+
"The Python interpreter at " + pythonPath + " does not have openrewrite " + version + ". " +
558+
"Either set pipPackagesPath to allow automatic installation, " +
559+
"or install the package manually: pip install openrewrite==" + version);
560+
}
561+
} else {
562+
// No version info (local dev build) — require the interpreter to already
563+
// have the rewrite package (e.g., from a venv with an editable install).
564+
if (!canImportRewrite(pythonPath)) {
565+
throw new IllegalStateException(
566+
"The Python interpreter at " + pythonPath + " cannot import the 'rewrite' package. " +
567+
"For development builds, run 'uv sync --extra dev' in the rewrite-python/rewrite/ " +
568+
"directory to set up an editable install, then configure pythonPath to point to " +
569+
"the venv's Python executable.");
570+
}
556571
}
557572

558573
Stream<@Nullable String> cmd;
@@ -594,10 +609,9 @@ public PythonRewriteRpc get() {
594609
// If debug source path is set, use it
595610
if (debugRewriteSourcePath != null) {
596611
pythonPathParts.add(debugRewriteSourcePath.toAbsolutePath().normalize().toString());
597-
} else if (pipPackagesPath == null) {
598-
// Only search for development source if pip packages path is not set
599-
// Try to find the Python source in the project structure
600-
// Look for rewrite-python/rewrite/src relative to the working directory
612+
} else if (version.isEmpty()) {
613+
// For local dev builds without a version file, try to find the Python
614+
// source in the project structure (as a fallback for PYTHONPATH)
601615
Path basePath = workingDirectory != null ? workingDirectory : Paths.get(System.getProperty("user.dir"));
602616

603617
// Check common locations
@@ -639,16 +653,14 @@ public PythonRewriteRpc get() {
639653
}
640654

641655
/**
642-
* Checks whether the given Python interpreter can already import the rewrite package
643-
* without any PYTHONPATH modifications. This detects venvs or system installs that
644-
* already have the openrewrite package available.
656+
* Checks whether the given Python interpreter can import the rewrite package
657+
* (any version) without PYTHONPATH modifications.
645658
*/
646659
private static boolean canImportRewrite(Path pythonPath) {
647660
try {
648661
Process probe = new ProcessBuilder(
649662
pythonPath.toString(), "-c", "import rewrite"
650663
).redirectErrorStream(true).start();
651-
// Drain output to prevent blocking
652664
try (InputStream is = probe.getInputStream()) {
653665
//noinspection StatementWithEmptyBody
654666
while (is.read() != -1) {
@@ -661,30 +673,47 @@ private static boolean canImportRewrite(Path pythonPath) {
661673
}
662674

663675
/**
664-
* Ensures the openrewrite Python package is installed in the pip packages directory.
665-
* This is required for the RPC server to start.
676+
* Checks whether the given Python interpreter has a specific version of the
677+
* openrewrite package installed.
666678
*/
667-
private void bootstrapOpenrewrite(Path pipPackagesPath, String version, boolean isDevBuild) {
668-
boolean pinVersion = !isDevBuild;
669-
670-
Path versionMarker = pipPackagesPath.resolve(".openrewrite-version");
671-
if (Files.exists(pipPackagesPath.resolve("rewrite"))) {
672-
// Already installed — check if version matches
673-
if (!pinVersion) {
674-
return;
675-
}
676-
try {
677-
if (Files.exists(versionMarker) &&
678-
version.equals(new String(Files.readAllBytes(versionMarker), StandardCharsets.UTF_8).trim())) {
679-
return; // Correct version already installed
680-
}
681-
} catch (IOException ignored) {
682-
// Can't read marker, reinstall to be safe
679+
private static boolean hasRewriteVersion(Path pythonPath, String version) {
680+
try {
681+
Process probe = new ProcessBuilder(
682+
pythonPath.toString(), "-c",
683+
"from importlib.metadata import version; print(version('openrewrite'))"
684+
).redirectErrorStream(true).start();
685+
String output;
686+
try (InputStream is = probe.getInputStream()) {
687+
output = StringUtils.readFully(is).trim();
683688
}
689+
return probe.waitFor(10, TimeUnit.SECONDS) && probe.exitValue() == 0
690+
&& version.equals(output);
691+
} catch (IOException | InterruptedException e) {
692+
return false;
684693
}
694+
}
685695

686-
String packageSpec = pinVersion ? "openrewrite==" + version : "openrewrite";
696+
/**
697+
* Checks whether the version-specific pip packages directory already has the
698+
* correct version installed, based on a marker file.
699+
*/
700+
private static boolean isVersionInstalled(Path pipPackagesPath, String version) {
701+
if (!Files.exists(pipPackagesPath.resolve("rewrite"))) {
702+
return false;
703+
}
704+
Path versionMarker = pipPackagesPath.resolve(".openrewrite-version");
705+
try {
706+
return Files.exists(versionMarker) &&
707+
version.equals(new String(Files.readAllBytes(versionMarker), StandardCharsets.UTF_8).trim());
708+
} catch (IOException e) {
709+
return false;
710+
}
711+
}
687712

713+
/**
714+
* Installs the pinned openrewrite package into the given pip packages directory.
715+
*/
716+
private void bootstrapOpenrewrite(Path pipPackagesPath, String version) {
688717
try {
689718
Files.createDirectories(pipPackagesPath);
690719

@@ -693,7 +722,7 @@ private void bootstrapOpenrewrite(Path pipPackagesPath, String version, boolean
693722
"-m", "pip", "install",
694723
"--upgrade",
695724
"--target=" + pipPackagesPath.toAbsolutePath().normalize(),
696-
packageSpec
725+
"openrewrite==" + version
697726
);
698727
pb.redirectErrorStream(true);
699728
if (log != null) {
@@ -727,10 +756,7 @@ private void bootstrapOpenrewrite(Path pipPackagesPath, String version, boolean
727756
throw new RuntimeException("Failed to bootstrap openrewrite package, pip install exited with code " + exitCode);
728757
}
729758

730-
// Write version marker so we can detect stale installs
731-
if (pinVersion) {
732-
Files.write(versionMarker, version.getBytes(StandardCharsets.UTF_8));
733-
}
759+
Files.write(pipPackagesPath.resolve(".openrewrite-version"), version.getBytes(StandardCharsets.UTF_8));
734760
} catch (IOException e) {
735761
throw new UncheckedIOException("Failed to bootstrap openrewrite package", e);
736762
} catch (InterruptedException e) {

0 commit comments

Comments
 (0)