Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions rewrite-python/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src/main/resources/META-INF/version.txt
32 changes: 31 additions & 1 deletion rewrite-python/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@file:Suppress("UnstableApiUsage")

import nl.javadude.gradle.plugins.license.LicenseExtension
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

Expand Down Expand Up @@ -196,7 +197,7 @@ tasks.withType<Test> {
// This is separate from Gradle because IntelliJ's Gradle integration doesn't support Python source roots.

// ============================================
// Python Publishing Tasks (PyPI)
// Version Resource (for RPC version pinning)
// ============================================

// Generate a PEP 440 compliant version for CI builds
Expand All @@ -211,6 +212,31 @@ val pythonVersion: String = if (System.getenv("CI") != null) {
project.version.toString().replace("-SNAPSHOT", ".dev0")
}

// Write version.txt resource so PythonRewriteRpc can pin the pip package version
val generateVersionTxt by tasks.registering {
group = "python"
description = "Generate META-INF/version.txt for RPC version pinning"

val versionTxt = file("src/main/resources/META-INF/version.txt")
inputs.property("version", pythonVersion)
outputs.file(versionTxt)

doLast {
versionTxt.parentFile.mkdirs()
versionTxt.writeText(pythonVersion)
}
}

listOf("sourcesJar", "processResources", "licenseMain", "assemble").forEach {
tasks.named(it) {
dependsOn(generateVersionTxt)
}
}

// ============================================
// Python Publishing Tasks (PyPI)
// ============================================

// Task to update version in pyproject.toml
val pythonUpdateVersion by tasks.registering {
group = "python"
Expand Down Expand Up @@ -364,3 +390,7 @@ val printTestClasspath by tasks.registering {
}
}

extensions.configure<LicenseExtension> {
exclude("**/version.txt")
}

Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import lombok.RequiredArgsConstructor;
import org.jspecify.annotations.Nullable;
import org.openrewrite.*;
import org.openrewrite.internal.StringUtils;
import org.openrewrite.marketplace.RecipeBundleResolver;
import org.openrewrite.marketplace.RecipeMarketplace;
import org.openrewrite.python.*;
Expand Down Expand Up @@ -519,8 +520,18 @@ public Builder pythonVersion(String pythonVersion) {

@Override
public PythonRewriteRpc get() {
// Bootstrap openrewrite package if pip packages path is set
if (pipPackagesPath != null) {
// For dev builds (version ending in .dev0), check whether the interpreter
// already has the rewrite package (e.g., from a venv with an editable install).
// If so, skip bootstrap and PYTHONPATH prepend so the interpreter's own
// version takes precedence. For release/CI builds, always use pipPackagesPath
// to ensure the correct pinned version.
String version = StringUtils.readFully(
PythonRewriteRpc.class.getResourceAsStream("/META-INF/version.txt"));
boolean isDevBuild = version.isEmpty() || version.endsWith(".dev0");
boolean interpreterHasRewrite = isDevBuild && pipPackagesPath != null && canImportRewrite(pythonPath);
boolean usePipPackagesPath = pipPackagesPath != null && !interpreterHasRewrite;

if (usePipPackagesPath) {
bootstrapOpenrewrite(pipPackagesPath);
}

Expand Down Expand Up @@ -550,8 +561,8 @@ public PythonRewriteRpc get() {
// Set up PYTHONPATH for the rewrite package
List<String> pythonPathParts = new ArrayList<>();

// Add pip packages path first if set (takes priority)
if (pipPackagesPath != null) {
// Add pip packages path if the interpreter doesn't already have rewrite
if (usePipPackagesPath) {
pythonPathParts.add(pipPackagesPath.toAbsolutePath().normalize().toString());
}

Expand Down Expand Up @@ -602,24 +613,64 @@ public PythonRewriteRpc get() {
}
}

/**
* Checks whether the given Python interpreter can already import the rewrite package
* without any PYTHONPATH modifications. This detects venvs or system installs that
* already have the openrewrite package available.
*/
private static boolean canImportRewrite(Path pythonPath) {
try {
Process probe = new ProcessBuilder(
pythonPath.toString(), "-c", "import rewrite"
).redirectErrorStream(true).start();
// Drain output to prevent blocking
try (InputStream is = probe.getInputStream()) {
//noinspection StatementWithEmptyBody
while (is.read() != -1) {
}
}
return probe.waitFor(10, TimeUnit.SECONDS) && probe.exitValue() == 0;
} catch (IOException | InterruptedException e) {
return false;
}
}

/**
* Ensures the openrewrite Python package is installed in the pip packages directory.
* This is required for the RPC server to start.
*/
private void bootstrapOpenrewrite(Path pipPackagesPath) {
Path rewriteModule = pipPackagesPath.resolve("rewrite");
if (Files.exists(rewriteModule)) {
return; // Already installed
String version = StringUtils.readFully(
PythonRewriteRpc.class.getResourceAsStream("/META-INF/version.txt"));
boolean pinVersion = !version.isEmpty() && !version.endsWith(".dev0");

Path versionMarker = pipPackagesPath.resolve(".openrewrite-version");
if (Files.exists(pipPackagesPath.resolve("rewrite"))) {
// Already installed — check if version matches
if (!pinVersion) {
return;
}
try {
if (Files.exists(versionMarker) &&
version.equals(new String(Files.readAllBytes(versionMarker), StandardCharsets.UTF_8).trim())) {
return; // Correct version already installed
}
} catch (IOException ignored) {
// Can't read marker, reinstall to be safe
}
}

String packageSpec = pinVersion ? "openrewrite==" + version : "openrewrite";

try {
Files.createDirectories(pipPackagesPath);

ProcessBuilder pb = new ProcessBuilder(
pythonPath.toString(),
"-m", "pip", "install",
"--upgrade",
"--target=" + pipPackagesPath.toAbsolutePath().normalize(),
"openrewrite"
packageSpec
);
pb.redirectErrorStream(true);
if (log != null) {
Expand Down Expand Up @@ -652,6 +703,11 @@ private void bootstrapOpenrewrite(Path pipPackagesPath) {
if (exitCode != 0) {
throw new RuntimeException("Failed to bootstrap openrewrite package, pip install exited with code " + exitCode);
}

// Write version marker so we can detect stale installs
if (pinVersion) {
Files.write(versionMarker, version.getBytes(StandardCharsets.UTF_8));
}
} catch (IOException e) {
throw new UncheckedIOException("Failed to bootstrap openrewrite package", e);
} catch (InterruptedException e) {
Expand Down