org.gradle.api.internal.classpath.DefaultModuleRegistry Maven / Gradle / Ivy
Show all versions of gradle-api Show documentation
/*
* Copyright 2011 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.classpath;
import com.google.common.collect.ImmutableList;
import org.gradle.api.UncheckedIOException;
import org.gradle.api.specs.Spec;
import org.gradle.cache.GlobalCache;
import org.gradle.internal.classpath.ClassPath;
import org.gradle.internal.classpath.DefaultClassPath;
import org.gradle.internal.installation.GradleInstallation;
import org.gradle.util.internal.GUtil;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
* Determines the classpath for a module by looking for a '${module}-classpath.properties' resource with 'name' set to the name of the module.
*/
public class DefaultModuleRegistry implements ModuleRegistry, GlobalCache {
private static final Spec SATISFY_ALL = element -> true;
private static final List JARS_REPLACED_BY_TD_PLUGIN = createJarsReplacedByTdPlugin();
private static List createJarsReplacedByTdPlugin() {
List jarsReplacedByTdPlugin = new ArrayList<>();
jarsReplacedByTdPlugin.add("junit-platform-commons-");
jarsReplacedByTdPlugin.add("junit-platform-engine-");
jarsReplacedByTdPlugin.add("junit-platform-launcher-");
jarsReplacedByTdPlugin.add("opentest4j-");
return jarsReplacedByTdPlugin;
}
@Nullable
private final GradleInstallation gradleInstallation;
private final Map modules = new HashMap<>();
private final Map externalModules = new HashMap<>();
private final List classpath = new ArrayList<>();
private final Map classpathJars = new LinkedHashMap<>();
public DefaultModuleRegistry(@Nullable GradleInstallation gradleInstallation) {
this(ClassPath.EMPTY, gradleInstallation);
}
public DefaultModuleRegistry(ClassPath additionalModuleClassPath, @Nullable GradleInstallation gradleInstallation) {
this(DefaultModuleRegistry.class.getClassLoader(), additionalModuleClassPath, gradleInstallation);
}
private DefaultModuleRegistry(ClassLoader classLoader, ClassPath additionalModuleClassPath, @Nullable GradleInstallation gradleInstallation) {
this.gradleInstallation = gradleInstallation;
for (File classpathFile : new EffectiveClassPath(classLoader).plus(additionalModuleClassPath).getAsFiles()) {
classpath.add(classpathFile);
if (classpathFile.isFile() && !classpathJars.containsKey(classpathFile.getName())) {
String jarFileName = classpathFile.getName();
classpathJars.put(jarFileName, classpathFile);
if (gradleInstallation == null) {
// Store the name without version as well, so we can look it up later
// See getNameOfJarReplacedByTestDistributionWithoutVersion why we are doing this
Optional replacedPrefix = getNameOfJarReplacedByTestDistributionWithoutVersion(jarFileName);
replacedPrefix.ifPresent(prefix -> classpathJars.put(prefix, classpathFile));
}
}
}
}
/**
* Determines the name without version when the JAR may have been replaced on the classpath.
*
* The Test Acceleration plugins from Develocity replace some JUnit platform JARS on the test classpath.
* Gradle's testing modules need the original JARs with a possible different version.
* When loading the dependencies from the classpath, this may cause a problem.
* Note that this happens only in integration tests in the Gradle codebase.
* As a workaround, we look up those dependency JARs without the version, so we can run embedded tests with Gradle.
* We only do the lookup when we don't have a Gradle installation to use.
*/
private static Optional getNameOfJarReplacedByTestDistributionWithoutVersion(String jarFileName) {
return jarFileName.endsWith(".jar")
? JARS_REPLACED_BY_TD_PLUGIN.stream()
.filter(jarFileName::startsWith)
.findAny()
: Optional.empty();
}
@Override
public List getGlobalCacheRoots() {
ImmutableList.Builder builder = ImmutableList.builder();
if (gradleInstallation != null) {
builder.addAll(gradleInstallation.getLibDirs());
}
builder.addAll(classpath);
return builder.build();
}
@Override
public ClassPath getAdditionalClassPath() {
return gradleInstallation == null ? DefaultClassPath.of(classpath) : ClassPath.EMPTY;
}
@Override
public Module getExternalModule(String name) {
Module module = externalModules.get(name);
if (module == null) {
module = loadExternalModule(name);
externalModules.put(name, module);
}
return module;
}
private Module loadExternalModule(String name) {
File externalJar = findJar(name, SATISFY_ALL);
if (externalJar == null) {
if (gradleInstallation == null) {
throw new UnknownModuleException(String.format("Cannot locate JAR for module '%s' in classpath: %s.", name, classpath));
}
throw new UnknownModuleException(String.format("Cannot locate JAR for module '%s' in distribution directory '%s'.", name, gradleInstallation.getGradleHome()));
}
return new DefaultModule(name, Collections.singleton(externalJar), Collections.emptySet());
}
@Override
public Module getModule(String name) {
Module module = modules.get(name);
if (module == null) {
module = loadModule(name);
modules.put(name, module);
}
return module;
}
@Override
@Nullable
public Module findModule(String name) {
Module module = modules.get(name);
if (module == null) {
module = loadOptionalModule(name);
if (module != null) {
modules.put(name, module);
}
}
return module;
}
private Module loadModule(String moduleName) {
Module module = loadOptionalModule(moduleName);
if (module != null) {
return module;
}
if (gradleInstallation == null) {
throw new UnknownModuleException(String.format("Cannot locate manifest for module '%s' in classpath: %s.", moduleName, classpath));
}
throw new UnknownModuleException(String.format("Cannot locate JAR for module '%s' in distribution directory '%s'.", moduleName, gradleInstallation.getGradleHome()));
}
private Module loadOptionalModule(final String moduleName) {
File jarFile = findJar(moduleName, jarFile1 -> hasModuleProperties(moduleName, jarFile1));
if (jarFile != null) {
Set implementationClasspath = new LinkedHashSet<>();
implementationClasspath.add(jarFile);
Properties properties = loadModuleProperties(moduleName, jarFile);
return module(moduleName, properties, implementationClasspath);
}
String resourceName = getClasspathManifestName(moduleName);
Set implementationClasspath = new LinkedHashSet<>();
findImplementationClasspath(moduleName, implementationClasspath);
for (File file : implementationClasspath) {
if (file.isDirectory()) {
File propertiesFile = new File(file, resourceName);
if (propertiesFile.isFile()) {
Properties properties = GUtil.loadProperties(propertiesFile);
return module(moduleName, properties, implementationClasspath);
}
}
}
return null;
}
private Module module(String moduleName, Properties properties, Set implementationClasspath) {
String[] runtimeJarNames = split(properties.getProperty("runtime"));
Set runtimeClasspath = findDependencyJars(moduleName, runtimeJarNames);
String[] projects = split(properties.getProperty("projects"));
String[] optionalProjects = split(properties.getProperty("optional"));
return new DefaultModule(moduleName, implementationClasspath, runtimeClasspath, projects, optionalProjects);
}
private Set findDependencyJars(String moduleName, String[] jarNames) {
Set runtimeClasspath = new LinkedHashSet<>();
for (String jarName : jarNames) {
runtimeClasspath.add(findDependencyJar(moduleName, jarName));
}
return runtimeClasspath;
}
private Set getModules(String[] projectNames) {
Set modules = new LinkedHashSet<>();
for (String project : projectNames) {
modules.add(getModule(project));
}
return modules;
}
private String[] split(String value) {
if (value == null) {
return new String[0];
}
value = value.trim();
if (value.length() == 0) {
return new String[0];
}
return value.split(",");
}
private void findImplementationClasspath(String name, Collection implementationClasspath) {
String projectDirName = projectDirNameFrom(name);
List suffixesForProjectDir = getClasspathSuffixesForProjectDir(projectDirName);
for (File file : classpath) {
if (file.isDirectory()) {
String path = file.getAbsolutePath();
for (String suffix : suffixesForProjectDir) {
if (path.endsWith(suffix)) {
implementationClasspath.add(file);
}
}
}
}
}
private static String projectDirNameFrom(String moduleName) {
Matcher matcher = Pattern.compile("gradle-(.+)").matcher(moduleName);
matcher.matches();
return matcher.group(1);
}
/**
* Provides the locations where the classes and resources of a Gradle module can be found
* when running in embedded mode from the IDE.
*
*
* - In Eclipse, they are in the bin/ folder.
* - In IDEA (native import), they are in the out/production/ folder.
*
* In both cases we also include the static and generated resources of the project.
*/
private List getClasspathSuffixesForProjectDir(String projectDirName) {
List suffixes = new ArrayList<>();
suffixes.add(("/" + projectDirName + "/out/production/classes").replace('/', File.separatorChar));
suffixes.add(("/" + projectDirName + "/out/production/resources").replace('/', File.separatorChar));
suffixes.add(("/" + projectDirName + "/bin").replace('/', File.separatorChar));
suffixes.add(("/" + projectDirName + "/src/main/resources").replace('/', File.separatorChar));
suffixes.add(("/" + projectDirName + "/build/classes/java/main").replace('/', File.separatorChar));
suffixes.add(("/" + projectDirName + "/build/classes/groovy/main").replace('/', File.separatorChar));
suffixes.add(("/" + projectDirName + "/build/resources/main").replace('/', File.separatorChar));
suffixes.add(("/" + projectDirName + "/build/generated-resources/main").replace('/', File.separatorChar));
suffixes.add(("/" + projectDirName + "/build/generated-resources/test").replace('/', File.separatorChar));
return suffixes;
}
private Properties loadModuleProperties(String name, File jarFile) {
try (ZipFile zipFile = new ZipFile(jarFile)) {
String entryName = getClasspathManifestName(name);
ZipEntry entry = zipFile.getEntry(entryName);
if (entry == null) {
throw new IllegalStateException("Did not find " + entryName + " in " + jarFile.getAbsolutePath());
}
try (InputStream is = zipFile.getInputStream(entry)) {
return GUtil.loadProperties(is);
}
} catch (IOException e) {
throw new UncheckedIOException(String.format("Could not load properties for module '%s' from %s", name, jarFile), e);
}
}
private boolean hasModuleProperties(String name, File jarFile) {
try (ZipFile zipFile = new ZipFile(jarFile)) {
String entryName = getClasspathManifestName(name);
ZipEntry entry = zipFile.getEntry(entryName);
return entry != null;
} catch (IOException e) {
throw new UncheckedIOException(String.format("Could not load properties for module '%s' from %s", name, jarFile), e);
}
}
private String getClasspathManifestName(String moduleName) {
return moduleName + "-classpath.properties";
}
private File findJar(String name, Spec allowedJarFiles) {
Pattern pattern = Pattern.compile(Pattern.quote(name) + "-\\d.*\\.jar");
if (gradleInstallation != null) {
for (File libDir : gradleInstallation.getLibDirs()) {
for (File file : libDir.listFiles()) {
if (pattern.matcher(file.getName()).matches()) {
return file;
}
}
}
}
for (File file : classpath) {
if (pattern.matcher(file.getName()).matches() && allowedJarFiles.isSatisfiedBy(file)) {
return file;
}
}
return null;
}
private File findDependencyJar(String module, String name) {
File jarFile = classpathJars.get(name);
if (jarFile != null) {
return jarFile;
}
if (gradleInstallation == null) {
// We only try to get a JAR with a different name in tests, when there is no Gradle installation
// See getNameOfJarReplacedByTestDistributionWithoutVersion why we are doing this
return getNameOfJarReplacedByTestDistributionWithoutVersion(name)
.map(classpathJars::get)
.orElseThrow(() -> new IllegalArgumentException(
String.format("Cannot find JAR '%s' required by module '%s' using classpath.", name, module)));
}
for (File libDir : gradleInstallation.getLibDirs()) {
jarFile = new File(libDir, name);
if (jarFile.isFile()) {
return jarFile;
}
}
throw new IllegalArgumentException(String.format("Cannot find JAR '%s' required by module '%s' using classpath or distribution directory '%s'", name, module, gradleInstallation.getGradleHome()));
}
private class DefaultModule implements Module {
private final String name;
private final String[] projects;
private final String[] optionalProjects;
private final ClassPath implementationClasspath;
private final ClassPath runtimeClasspath;
private final ClassPath classpath;
public DefaultModule(String name, Set implementationClasspath, Set runtimeClasspath, String[] projects, String[] optionalProjects) {
this.name = name;
this.projects = projects;
this.optionalProjects = optionalProjects;
this.implementationClasspath = DefaultClassPath.of(implementationClasspath);
this.runtimeClasspath = DefaultClassPath.of(runtimeClasspath);
Set classpath = new LinkedHashSet<>();
classpath.addAll(implementationClasspath);
classpath.addAll(runtimeClasspath);
this.classpath = DefaultClassPath.of(classpath);
}
public DefaultModule(String name, Set singleton, Set files) {
this(name, singleton, files, NO_PROJECTS, NO_PROJECTS);
}
@Override
public String toString() {
return "module '" + name + "'";
}
@Override
public Set getRequiredModules() {
return getModules(projects);
}
@Override
public ClassPath getImplementationClasspath() {
return implementationClasspath;
}
@Override
public ClassPath getRuntimeClasspath() {
return runtimeClasspath;
}
@Override
public ClassPath getClasspath() {
return classpath;
}
@Override
public Set getAllRequiredModules() {
Set modules = new LinkedHashSet<>();
collectRequiredModules(modules);
return modules;
}
@Override
public ClassPath getAllRequiredModulesClasspath() {
ClassPath classPath = ClassPath.EMPTY;
for (Module module : getAllRequiredModules()) {
classPath = classPath.plus(module.getClasspath());
}
return classPath;
}
private void collectRequiredModules(Set modules) {
if (!modules.add(this)) {
return;
}
for (Module module : getRequiredModules()) {
collectDependenciesOf(module, modules);
}
for (String optionalProject : optionalProjects) {
Module module = findModule(optionalProject);
if (module != null) {
collectDependenciesOf(module, modules);
}
}
}
private void collectDependenciesOf(Module module, Set modules) {
((DefaultModule) module).collectRequiredModules(modules);
}
private Module findModule(String optionalProject) {
try {
return getModule(optionalProject);
} catch (UnknownModuleException ex) {
return null;
}
}
}
private static final String[] NO_PROJECTS = new String[0];
}