Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,15 @@
import org.gradle.util.GradleVersion;
import org.gradle.work.DisableCachingByDefault;
import org.jspecify.annotations.Nullable;
import org.openrewrite.gradle.dependencies.ResolvedDependencies;
import org.openrewrite.gradle.dependencies.ResolvedDependenciesProvider;

import javax.inject.Inject;
import java.io.File;
import java.nio.file.Path;
import java.util.List;
import java.util.Set;

import static java.util.Collections.emptySet;
import static java.util.stream.Collectors.toSet;

@DisableCachingByDefault(because = "Rewrite tasks act on source files in place and are not safe to cache")
public abstract class AbstractRewriteTask extends DefaultTask {
protected @Nullable Provider<Set<File>> resolvedDependencies;
protected @Nullable Provider<ResolvedDependencies> resolvedDependencies;
protected boolean dumpGcActivity;
protected @Nullable GradleProjectParser gpp;
protected @Nullable RewriteExtension extension;
Expand All @@ -53,7 +49,7 @@ public <T extends AbstractRewriteTask> T setExtension(RewriteExtension extension
return (T) this;
}

public <T extends AbstractRewriteTask> T setResolvedDependencies(Provider<Set<File>> resolvedDependencies) {
public <T extends AbstractRewriteTask> T setResolvedDependencies(Provider<ResolvedDependencies> resolvedDependencies) {
this.resolvedDependencies = resolvedDependencies;
//noinspection unchecked
return (T) this;
Expand Down Expand Up @@ -83,14 +79,11 @@ protected <T extends GradleProjectParser> T getProjectParser() {
if (resolvedDependencies == null) {
throw new IllegalArgumentException("Must configure resolvedDependencies");
}
Set<File> deps = resolvedDependencies.getOrNull();
ResolvedDependencies deps = resolvedDependencies.getOrNull();
if (deps == null) {
deps = emptySet();
deps = ResolvedDependenciesProvider.empty();
}
Set<Path> classpath = deps.stream()
.map(File::toPath)
.collect(toSet());
gpp = new DelegatingProjectParser(getProject(), extension, classpath);
gpp = new DelegatingProjectParser(getProject(), extension, deps);
}
//noinspection unchecked
return (T) gpp;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2025 the original author or authors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.gradle;

import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;

/**
* Class loader passed to {@code rewrite-core}'s {@link org.openrewrite.config.Environment Environment}
* so it can discover recipes from multiple class loaders.
* In our case, they are from {@code RewritePlugin.knownRewriteDependencies} and recipes in the {@code rewrite} configuration.
*/
public class CompositeURLClassLoader extends URLClassLoader {

private final Collection<URLClassLoader> loaders;

private CompositeURLClassLoader(Collection<URLClassLoader> loaders) {
super(loaders.stream().flatMap(cl -> Arrays.stream(cl.getURLs())).toArray(URL[]::new));
this.loaders = loaders;
}

public CompositeURLClassLoader(URLClassLoader... loaders) {
this(new ArrayList<>(Arrays.asList(loaders)));
}

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
for (URLClassLoader loader : loaders) {
try {
return loader.loadClass(name);
} catch (ClassNotFoundException e) {
// Ignore
}
}

throw new ClassNotFoundException(name);
}
}
152 changes: 122 additions & 30 deletions plugin/src/main/java/org/openrewrite/gradle/DelegatingProjectParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,70 +18,107 @@
import org.gradle.api.Project;
import org.gradle.internal.service.ServiceRegistry;
import org.jspecify.annotations.Nullable;
import org.openrewrite.gradle.dependencies.ProjectDependency;
import org.openrewrite.gradle.dependencies.ResolvedDependencies;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.net.URLClassLoader;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.function.Consumer;

import static java.util.Collections.sort;
import static java.util.stream.Collectors.toList;

public class DelegatingProjectParser implements GradleProjectParser {
@Nullable
protected static List<URL> rewriteClasspath;
protected static List<String> rewriteClasspathFingerprint;
@Nullable
protected static List<String> recipeClasspathFingerprint;
@Nullable
protected static RewriteClassLoader rewriteClassLoader;
@Nullable
protected static URLClassLoader recipeClassLoader;
protected final GradleProjectParser gpp;

public DelegatingProjectParser(Project project, RewriteExtension extension, Set<Path> classpath) {
public DelegatingProjectParser(Project project, RewriteExtension extension, ResolvedDependencies classpath) {
try {
List<URL> classpathUrls = classpath.stream()
.map(Path::toUri)
.map(uri -> {
try {
return uri.toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
})
List<URL> rewriteClasspathUrls = classpath.getFromRewriteOnly().stream()
.map(ProjectDependency::getUrl)
.collect(toList());
List<URL> recipeClasspathUrls = classpath.getFromRecipeOnly().stream()
.map(ProjectDependency::getUrl)
.collect(toList());

@SuppressWarnings("ConstantConditions")
URL currentJar = jarContainingResource(getClass()
.getResource("/org/openrewrite/gradle/isolated/DefaultProjectParser.class")
.toString());
classpathUrls.add(currentJar);
rewriteClasspathUrls.add(currentJar);

List<Path> rewriteClasspathEntries = classpath.getFromRewriteOnly().stream().map(ProjectDependency::getPath).collect(toList());
List<Path> recipeClasspathEntries = classpath.getFromRecipeOnly().stream().map(ProjectDependency::getPath).collect(toList());
rewriteClasspathEntries.add(Paths.get(currentJar.toURI()));

ClassLoader pluginClassLoader = getPluginClassLoader(project);
List<String> newRewriteClasspathFingerprint = fingerprint(rewriteClasspathEntries);
List<String> newRecipeClasspathFingerprint = fingerprint(recipeClasspathEntries);

if (rewriteClassLoader == null ||
!classpathUrls.equals(rewriteClasspath) ||
rewriteClassLoader.getPluginClassLoader() != pluginClassLoader) {
if (rewriteClassLoader != null) {
rewriteClassLoader.close();
}
rewriteClassLoader = new RewriteClassLoader(classpathUrls, pluginClassLoader);
rewriteClasspath = classpathUrls;
// Throw recipe CL if rewrite CL is reset
// because recipe classes depend on rewrite's dependencies
if (hasRewriteClasspathChanged(newRewriteClasspathFingerprint, pluginClassLoader)) {
recreateRewriteClassLoader(rewriteClasspathUrls, newRewriteClasspathFingerprint, pluginClassLoader);
recreateRecipeClassLoader(recipeClasspathUrls, newRecipeClasspathFingerprint);
} else if (hasRecipeClasspathChanged(newRecipeClasspathFingerprint)) {
recreateRecipeClassLoader(recipeClasspathUrls, newRecipeClasspathFingerprint);
}

Class<?> gppClass = Class.forName("org.openrewrite.gradle.isolated.DefaultProjectParser", true, rewriteClassLoader);
Class<?> gppClass = Class.forName("org.openrewrite.gradle.isolated.DefaultProjectParser", true, recipeClassLoader);
assert (gppClass.getClassLoader() == rewriteClassLoader) : "DefaultProjectParser must be loaded from RewriteClassLoader to be sufficiently isolated from Gradle's classpath";
gpp = (GradleProjectParser) gppClass.getDeclaredConstructor(Project.class, RewriteExtension.class)
.newInstance(project, extension);
gpp = (GradleProjectParser) gppClass.getDeclaredConstructor(Project.class, RewriteExtension.class, ClassLoader.class)
.newInstance(project, extension, new CompositeURLClassLoader(rewriteClassLoader, recipeClassLoader));

} catch (Exception e) {
throw new RuntimeException(e);
}
}

private static boolean hasRewriteClasspathChanged(@Nullable List<String> newRewriteClasspathFingerprint, ClassLoader pluginClassLoader) {
return rewriteClassLoader == null ||
rewriteClasspathFingerprint == null ||
!rewriteClasspathFingerprint.equals(newRewriteClasspathFingerprint) ||
rewriteClassLoader.getPluginClassLoader() != pluginClassLoader;
}

private static boolean hasRecipeClasspathChanged(@Nullable List<String> newRecipeClasspathFingerprint) {
return recipeClassLoader == null ||
recipeClasspathFingerprint == null ||
!recipeClasspathFingerprint.equals(newRecipeClasspathFingerprint);
}

private static void recreateRewriteClassLoader(List<URL> rewriteClasspathUrls, @Nullable List<String> newRewriteClasspathFingerprint, ClassLoader pluginClassLoader) throws IOException {
if (rewriteClassLoader != null) {
discard(rewriteClassLoader);
}

rewriteClassLoader = new RewriteClassLoader(rewriteClasspathUrls, pluginClassLoader);
rewriteClasspathFingerprint = newRewriteClasspathFingerprint;
}

private static void recreateRecipeClassLoader(List<URL> recipeClasspathUrls, @Nullable List<String> newRecipeClasspathFingerprint) throws IOException {
if (recipeClassLoader != null) {
discard(recipeClassLoader);
}

recipeClassLoader = new URLClassLoader(recipeClasspathUrls.toArray(new URL[0]), Objects.requireNonNull(rewriteClassLoader, "Rewrite CL is missing"));
recipeClasspathFingerprint = newRecipeClasspathFingerprint;
}

@Override
public List<String> getActiveRecipes() {
return unwrapInvocationException(gpp::getActiveRecipes);
Expand Down Expand Up @@ -134,6 +171,61 @@ public void shutdownRewrite() {
});
}

private static void discard(URLClassLoader classLoader) throws IOException {
try {
Class.forName("org.openrewrite.gradle.isolated.DefaultProjectParser", true, classLoader)
.getMethod("cleanCurrentClassLoader")
.invoke(null);
} catch (ReflectiveOperationException | LinkageError ignored) {
// Not all versions of rewrite bundle JGit, in which case there is no work queue to shut down
}
classLoader.close();
}

/**
* Recipe jars built by the project itself are replaced in place, keeping the same location on the classpath.
* Comparing locations alone would then reuse a {@link RewriteClassLoader} holding the previous recipe classes
* for as long as the Gradle daemon lives, so compare the contents of each classpath entry as well.
*
* @return a fingerprint per classpath entry, or {@code null} if any entry could not be read
*/
static @Nullable List<String> fingerprint(Collection<Path> classpath) {
List<String> fingerprints = new ArrayList<>(classpath.size());
for (Path classpathEntry : classpath) {
try {
fingerprints.add(fingerprint(classpathEntry));
} catch (IOException e) {
return null;
}
}
sort(fingerprints);
return fingerprints;
}

private static String fingerprint(Path classpathEntry) throws IOException {
BasicFileAttributes attributes = Files.readAttributes(classpathEntry, BasicFileAttributes.class);
if (!attributes.isDirectory()) {
return classpathEntry + "|" + stamp(classpathEntry, attributes);
}
DirectoryStamp directoryStamp = new DirectoryStamp();
Files.walkFileTree(classpathEntry, directoryStamp);
return classpathEntry + "|" + directoryStamp.stamp;
}

private static long stamp(Path file, BasicFileAttributes attributes) {
return 31L * (31L * file.hashCode() + attributes.size()) + attributes.lastModifiedTime().toMillis();
}

private static class DirectoryStamp extends SimpleFileVisitor<Path> {
private long stamp;

@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) {
stamp += stamp(file, attributes);
return FileVisitResult.CONTINUE;
}
}

protected URL jarContainingResource(String resourcePath) {
try {
if (resourcePath.startsWith("jar:")) {
Expand Down
Loading