Skip to content
Closed
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 @@ -17,6 +17,7 @@

import groovy.lang.Closure;
import org.gradle.api.Action;
import org.gradle.api.Incubating;
import org.gradle.api.artifacts.Dependency;
import org.gradle.api.artifacts.ExternalModuleDependency;
import org.gradle.api.artifacts.MinimalExternalModuleDependency;
Expand Down Expand Up @@ -383,6 +384,15 @@ public interface DependencyHandler extends ExtensionAware {
*/
Dependency gradleApi();

/**
* Creates a dependency on the API of the specified version of Gradle.
*
* @return The dependency.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to add some Javadoc on the expected format of the parameter. Are snapshots accepted?

I think we need to mention that only older versions are accepted, right? What happens if I give it a newer or non-existent version?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, probably. Though I wouldn't go into polishing the Javadoc for this spike.

* @since 7.5
*/
@Incubating
Dependency gradleApi(String version);

/**
* Creates a dependency on the <a href="https://docs.gradle.org/current/userguide/test_kit.html" target="_top">Gradle test-kit</a> API.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,4 @@ import org.apache.commons.math3.util.FastMath
args("--init-script", initScript.toString())
succeeds("help")
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright 2022 the original author or authors.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.gradle.api.internal.artifacts;

import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.Dependency;
import org.gradle.api.artifacts.SelfResolvingDependency;
import org.gradle.api.artifacts.dsl.DependencyHandler;
import org.gradle.api.artifacts.dsl.RepositoryHandler;

import java.io.File;
import java.util.Collection;
import java.util.Optional;
import java.util.Set;

public class GradleApiVersionProvider {

public static final String GRADLE_API_SOURCE_VERSION_PROPERTY = "org.gradle.api.source-version";

public static Optional<String> getGradleApiSourceVersion() {
return Optional.ofNullable(System.getProperty(GRADLE_API_SOURCE_VERSION_PROPERTY));
}

public static void addGradleSourceApiRepository(RepositoryHandler repositoryHandler) {
getGradleApiSourceVersion().ifPresent(version -> {
String repositoryUrl = System.getProperty("gradle.api.repository.url", "https://repo.gradle.org/gradle/libs-releases");
repositoryHandler.maven(repo -> repo.setUrl(repositoryUrl));
});
}

public static void addToConfiguration(Configuration configuration, DependencyHandler repositoryHandler) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's maybe not that important here, but could we return dependency here and the caller would do whatever it wants? Similar also for the repository.

For this case:

GradleApiVersionProvider.addToConfiguration(project.getConfigurations().getByName(API_CONFIGURATION), dependencies);
GradleApiVersionProvider.addGradleSourceApiRepository(project.getRepositories());

It feels a bit weird to me that we accept something as a parameter and then we add something to it, instead of that we provide a dependency/repository and the caller would do with it whatever it wants.

Dependency gradleApiDependency = getGradleApiSourceVersion()
.map(repositoryHandler::gradleApi)
.orElseGet(repositoryHandler::gradleApi);
configuration.getDependencies().add(gradleApiDependency);
}

public static Collection<File> resolveGradleSourceApi(DependencyResolutionServices dependencyResolutionServices) {
return getGradleApiSourceVersion()
.map(version -> gradleApisFromRepository(dependencyResolutionServices, version))
.orElseGet(() -> gradleApisFromCurrentGradle(dependencyResolutionServices.getDependencyHandler()));
}

private static Set<File> gradleApisFromCurrentGradle(DependencyHandler dependencyHandler) {
SelfResolvingDependency gradleApiDependency = (SelfResolvingDependency) dependencyHandler.gradleApi();
return gradleApiDependency.resolve();

}
private static Set<File> gradleApisFromRepository(DependencyResolutionServices dependencyResolutionServices, String version) {
addGradleSourceApiRepository(dependencyResolutionServices.getResolveRepositoryHandler());
Configuration detachedConfiguration = dependencyResolutionServices.getConfigurationContainer().detachedConfiguration(dependencyResolutionServices.getDependencyHandler().gradleApi(version));
return detachedConfiguration.resolve();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.gradle.groovy.scripts.Transformer;
import org.gradle.internal.UncheckedException;
import org.gradle.internal.classloader.ClassLoaderUtils;
import org.gradle.internal.classloader.FilteringClassLoader;
import org.gradle.internal.classloader.ImplementationHashAware;
import org.gradle.internal.classloader.VisitableURLClassLoader;
import org.gradle.internal.classpath.ClassPath;
Expand Down Expand Up @@ -107,18 +108,43 @@ public void compileToDir(ScriptSource source, ClassLoader classLoader, File clas
}

private void compileScript(ScriptSource source, ClassLoader classLoader, CompilerConfiguration configuration, File metadataDir,
final CompileOperation<?> extractingTransformer, final Action<? super ClassNode> customVerifier) {
final CompileOperation<?> extractingTransformer, final Action<? super ClassNode> customVerifier
) {
final Transformer transformer = extractingTransformer != null ? extractingTransformer.getTransformer() : null;
logger.info("Compiling {} using {}.", source.getDisplayName(), transformer != null ? transformer.getClass().getSimpleName() : "no transformer");

final EmptyScriptDetector emptyScriptDetector = new EmptyScriptDetector();
final PackageStatementDetector packageDetector = new PackageStatementDetector();

FilteringClassLoader.Spec groovyCompilerClassLoaderSpec = new FilteringClassLoader.Spec();
groovyCompilerClassLoaderSpec.allowPackage("org.codehaus.groovy");
groovyCompilerClassLoaderSpec.allowPackage("groovy");
groovyCompilerClassLoaderSpec.allowPackage("groovyjarjarasm");
// Disallow classes from Groovy Jar that reference external classes. Such classes must be loaded from astTransformClassLoader,
// or a NoClassDefFoundError will occur. Essentially this is drawing a line between the Groovy compiler and the Groovy
// library, albeit only for selected classes that run a high risk of being statically referenced from a transform.
groovyCompilerClassLoaderSpec.disallowClass("groovy.util.GroovyTestCase");
groovyCompilerClassLoaderSpec.disallowClass("org.codehaus.groovy.transform.NotYetImplementedASTTransformation");
groovyCompilerClassLoaderSpec.disallowPackage("groovy.servlet");
FilteringClassLoader groovyCompilerClassLoader = new FilteringClassLoader(GroovyClassLoader.class.getClassLoader(), groovyCompilerClassLoaderSpec);

// AST transforms need their own class loader that shares compiler classes with the compiler itself
// can't delegate to compileClasspathLoader because this would result in ASTTransformation interface
// (which is implemented by the transform class) being loaded by compileClasspathClassLoader (which is
// where the transform class is loaded from)
GroovyClassLoader astTransformClassLoader = new GroovyClassLoader(groovyCompilerClassLoader, null);

GroovyClassLoader groovyClassLoader = new GroovyClassLoader(classLoader, configuration, false) {
@Override
protected CompilationUnit createCompilationUnit(CompilerConfiguration compilerConfiguration,
CodeSource codeSource) {

CompilationUnit compilationUnit = new CustomCompilationUnit(compilerConfiguration, codeSource, customVerifier, this, simpleNameToFQN);
CompilationUnit compilationUnit = new CustomCompilationUnit(compilerConfiguration, codeSource, customVerifier, this, simpleNameToFQN) {
@Override
public GroovyClassLoader getTransformLoader() {
return astTransformClassLoader;
}
};

if (transformer != null) {
transformer.register(compilationUnit);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URLClassLoader;

import static org.gradle.internal.classpath.CachedClasspathTransformer.StandardTransform.BuildLogic;

Expand All @@ -63,30 +64,39 @@ public class FileCacheBackedScriptClassCompiler implements ScriptClassCompiler,
private final GlobalScopedCache cacheRepository;
private final ClassLoaderHierarchyHasher classLoaderHierarchyHasher;
private final CachedClasspathTransformer classpathTransformer;
private final GroovyScriptClasspathProvider groovyScriptClasspathProvider;

public FileCacheBackedScriptClassCompiler(
GlobalScopedCache cacheRepository, ScriptCompilationHandler scriptCompilationHandler,
ProgressLoggerFactory progressLoggerFactory, ClassLoaderHierarchyHasher classLoaderHierarchyHasher,
CachedClasspathTransformer classpathTransformer) {
GlobalScopedCache cacheRepository,
ScriptCompilationHandler scriptCompilationHandler,
ProgressLoggerFactory progressLoggerFactory,
ClassLoaderHierarchyHasher classLoaderHierarchyHasher,
CachedClasspathTransformer classpathTransformer,
GroovyScriptClasspathProvider groovyScriptClasspathProvider
) {
this.cacheRepository = cacheRepository;
this.scriptCompilationHandler = scriptCompilationHandler;
this.progressLoggerFactory = progressLoggerFactory;
this.classLoaderHierarchyHasher = classLoaderHierarchyHasher;
this.classpathTransformer = classpathTransformer;
this.groovyScriptClasspathProvider = groovyScriptClasspathProvider;
}

@Override
public <T extends Script, M> CompiledScript<T, M> compile(final ScriptSource source,
final ClassLoaderScope targetScope,
final CompileOperation<M> operation,
final Class<T> scriptBaseClass,
final Action<? super ClassNode> verifier) {
final Action<? super ClassNode> verifier
) {
assert source.getResource().isContentCached();
if (source.getResource().getHasEmptyContent()) {
return emptyCompiledScript(operation);
}

ClassLoader classLoader = targetScope.getExportClassLoader();
ClassPath compilationClasspath = groovyScriptClasspathProvider.compilationClassPathOf(targetScope);
ClassLoader compileClassLoader = new URLClassLoader(compilationClasspath.getAsURLArray(), ClassLoader.getSystemClassLoader().getParent());
HashCode sourceHashCode = source.getResource().getContentHash();
final String dslId = operation.getId();
HashCode classLoaderHash = classLoaderHierarchyHasher.getClassLoaderHash(classLoader);
Expand All @@ -99,6 +109,10 @@ public <T extends Script, M> CompiledScript<T, M> compile(final ScriptSource sou
hasher.putString(dslId);
hasher.putHash(sourceHashCode);
hasher.putHash(classLoaderHash);
for (File jar : compilationClasspath.getAsFiles()) {
// TODO: Snapshot the contents here.
hasher.putString(jar.getName());
}
String key = hasher.hash().toCompactString();

// Caching involves 2 distinct caches, so that 2 scripts with the same (hash, classpath) do not get compiled twice
Expand All @@ -111,7 +125,7 @@ public <T extends Script, M> CompiledScript<T, M> compile(final ScriptSource sou
.withDisplayName(dslId + " generic class cache for " + source.getDisplayName())
.withInitializer(new ProgressReportingInitializer(
progressLoggerFactory,
new CompileToCrossBuildCacheAction(remapped, classLoader, operation, verifier, scriptBaseClass),
new CompileToCrossBuildCacheAction(remapped, compileClassLoader, operation, verifier, scriptBaseClass),
"Compiling " + source.getShortDisplayName()))
.open();
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright 2022 the original author or authors.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.gradle.groovy.scripts.internal;

import org.gradle.api.internal.initialization.ClassLoaderScope;
import org.gradle.internal.classloader.ClassLoaderVisitor;
import org.gradle.internal.classpath.ClassPath;
import org.gradle.internal.classpath.DefaultClassPath;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Supplier;

public class GroovyScriptClasspathProvider {
private final ConcurrentMap<ClassLoaderScope, ClassPath> cachedScopeCompilationClasspath = new ConcurrentHashMap<>();
private final Map<ClassLoader, Set<File>> cachedClasspaths = new HashMap<>();
private final Set<File> gradleImplementationClasspath = new LinkedHashSet<>();
private ClassPath gradleApi;
private ClassPath groovyJars;
private final Supplier<Collection<File>> gradleApiJarsProvider;
private final Supplier<Set<File>> groovyJarsProvider;

public GroovyScriptClasspathProvider(
ClassLoaderScope coreAndPluginsScope,
Supplier<Collection<File>> gradleApiJarsProvider,
Supplier<Set<File>> groovyJarsProvider
) {
this.gradleApiJarsProvider = gradleApiJarsProvider;
this.groovyJarsProvider = groovyJarsProvider;
this.gradleImplementationClasspath.addAll(classpathOf(coreAndPluginsScope.getExportClassLoader()));
this.gradleImplementationClasspath.removeIf(file -> !file.getName().startsWith("gradle-"));
}

public ClassPath compilationClassPathOf(ClassLoaderScope scope) {
return getGradleApi().plus(exportClassPathFromHierarchyOf(scope));
}

private ClassPath getGradleApi() {
if (gradleApi == null) {
gradleApi = DefaultClassPath.of(gradleApiJarsProvider.get());
}
return gradleApi;
}

private ClassPath getGroovy() {
if (groovyJars == null) {
groovyJars = DefaultClassPath.of(groovyJarsProvider.get());
}
return groovyJars;
}

private ClassPath exportClassPathFromHierarchyOf(ClassLoaderScope scope) {
Set<File> exportedClasspath = new LinkedHashSet<>(classpathOf(scope.getExportClassLoader()));
exportedClasspath.removeAll(gradleImplementationClasspath);
return DefaultClassPath.of(exportedClasspath);
}

private Set<File> classpathOf(ClassLoader classLoader) {
Set<File> classpath = cachedClasspaths.get(classLoader);
if (classpath == null) {
Set<File> classpathFiles = new LinkedHashSet<>();
new ClassLoaderVisitor() {
@Override
public void visitClassPath(URL[] classPath) {
for (URL url : classPath) {
if (url.getProtocol().equals("file")) {
classpathFiles.add(new File(toUri(url)));
}
}
}

@Override
public void visitParent(ClassLoader parent) {
classpathFiles.addAll(classpathOf(parent));
}

private URI toUri(URL url) {
try {
return url.toURI();
} catch (URISyntaxException e) {
try {
return new URL(
url.getProtocol(),
url.getHost(),
url.getPort(),
url.getFile().replace(" ", "%20")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to encode any other character here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copied that from the Kotlin version of this class which seems to work. I suppose we should have only one classpath provider and not two separate implementations doing mostly the same things.

).toURI();
} catch (URISyntaxException | MalformedURLException ex) {
throw new RuntimeException(ex);
}
}
}
}.visit(classLoader);
cachedClasspaths.put(classLoader, classpathFiles);
classpath = classpathFiles;
}
return classpath;
}
}
Loading