Skip to content

Commit 417f806

Browse files
committed
Support registration of additional tasks for 'requires runtime' support
This is needed primarily for whitebox testing. #20
1 parent a4a590a commit 417f806

File tree

9 files changed

+191
-52
lines changed

9 files changed

+191
-52
lines changed

src/main/java/org/gradlex/javamodule/dependencies/JavaModuleDependenciesExtension.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,29 @@
1616

1717
package org.gradlex.javamodule.dependencies;
1818

19+
import org.gradle.api.Task;
1920
import org.gradle.api.artifacts.VersionCatalog;
2021
import org.gradle.api.artifacts.VersionCatalogsExtension;
2122
import org.gradle.api.artifacts.VersionConstraint;
23+
import org.gradle.api.file.ConfigurableFileCollection;
24+
import org.gradle.api.file.Directory;
25+
import org.gradle.api.file.FileCollection;
26+
import org.gradle.api.file.ProjectLayout;
27+
import org.gradle.api.model.ObjectFactory;
2228
import org.gradle.api.provider.MapProperty;
2329
import org.gradle.api.provider.Property;
2430
import org.gradle.api.provider.Provider;
31+
import org.gradle.api.tasks.SourceSet;
32+
import org.gradle.api.tasks.compile.JavaCompile;
33+
import org.gradle.api.tasks.javadoc.Javadoc;
34+
import org.gradlex.javamodule.dependencies.internal.compile.AddSyntheticModulesToCompileClasspathAction;
35+
import org.gradlex.javamodule.dependencies.internal.utils.ModuleInfo;
36+
import org.gradlex.javamodule.dependencies.internal.utils.ModuleInfoCache;
2537

2638
import javax.annotation.Nullable;
39+
import javax.inject.Inject;
2740
import java.util.HashMap;
41+
import java.util.List;
2842
import java.util.Map;
2943
import java.util.Optional;
3044

@@ -40,6 +54,7 @@ public abstract class JavaModuleDependenciesExtension {
4054
static final String JAVA_MODULE_DEPENDENCIES = "javaModuleDependencies";
4155

4256
private final VersionCatalogsExtension versionCatalogs;
57+
private final ModuleInfoCache moduleInfoCache;
4358

4459
/**
4560
* @return the mappings from Module Name to GA coordinates; can be modified
@@ -70,6 +85,7 @@ public abstract class JavaModuleDependenciesExtension {
7085

7186
public JavaModuleDependenciesExtension(VersionCatalogsExtension versionCatalogs) {
7287
this.versionCatalogs = versionCatalogs;
88+
this.moduleInfoCache = getObjects().newInstance(ModuleInfoCache.class);
7389
getWarnForMissingVersions().convention(versionCatalogs != null);
7490
getVersionCatalogName().convention("libs");
7591
getModuleNameToGA().putAll(SharedMappings.mappings);
@@ -142,4 +158,48 @@ public String moduleName(String ga) {
142158
}
143159
return null;
144160
}
161+
162+
/**
163+
* Adds support for compiling module-info.java in the given source set with the given task,
164+
* if 'requires runtime' dependencies are used.
165+
*
166+
* @param task The task that compiles code from the given source set
167+
* @param sourceSet The source set that contains the module-info.java
168+
*/
169+
public FileCollection addRequiresRuntimeSupport(JavaCompile task, SourceSet sourceSet) {
170+
return doAddRequiresRuntimeSupport(task, sourceSet);
171+
}
172+
173+
/**
174+
* Adds support for generating Javadoc for the module-info.java in the given source set with the given task,
175+
* if 'requires runtime' dependencies are used.
176+
*
177+
* @param task The task that generates Javadoc from the given source set
178+
* @param sourceSet The source set that contains the module-info.java
179+
*/
180+
public FileCollection addRequiresRuntimeSupport(Javadoc task, SourceSet sourceSet) {
181+
return doAddRequiresRuntimeSupport(task, sourceSet);
182+
}
183+
184+
FileCollection doAddRequiresRuntimeSupport(Task task, SourceSet sourceSet) {
185+
List<String> requiresRuntime = getModuleInfoCache().get(sourceSet).get(ModuleInfo.Directive.REQUIRES_RUNTIME);
186+
ConfigurableFileCollection syntheticModuleInfoFolders = getObjects().fileCollection();
187+
if (!requiresRuntime.isEmpty()) {
188+
Provider<Directory> tmpDir = getLayout().getBuildDirectory().dir("tmp/java-module-dependencies/" + task.getName());
189+
requiresRuntime.forEach(moduleName -> syntheticModuleInfoFolders.from(tmpDir.map(dir -> dir.dir(moduleName))));
190+
task.doFirst(getObjects().newInstance(AddSyntheticModulesToCompileClasspathAction.class, syntheticModuleInfoFolders));
191+
return syntheticModuleInfoFolders;
192+
}
193+
return syntheticModuleInfoFolders;
194+
}
195+
196+
@Inject
197+
protected abstract ObjectFactory getObjects();
198+
199+
@Inject
200+
protected abstract ProjectLayout getLayout();
201+
202+
ModuleInfoCache getModuleInfoCache() {
203+
return moduleInfoCache;
204+
}
145205
}

src/main/java/org/gradlex/javamodule/dependencies/JavaModuleDependenciesPlugin.java

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,13 @@
2727
import org.gradle.api.artifacts.VersionCatalog;
2828
import org.gradle.api.artifacts.VersionCatalogsExtension;
2929
import org.gradle.api.artifacts.VersionConstraint;
30-
import org.gradle.api.file.RegularFile;
3130
import org.gradle.api.plugins.JavaPlugin;
3231
import org.gradle.api.provider.Provider;
3332
import org.gradle.api.tasks.SourceSet;
3433
import org.gradle.api.tasks.SourceSetContainer;
3534
import org.gradle.api.tasks.TaskProvider;
3635
import org.gradle.util.GradleVersion;
3736
import org.gradlex.javamodule.dependencies.internal.bridges.ExtraJavaModuleInfoBridge;
38-
import org.gradlex.javamodule.dependencies.internal.compile.AddSyntheticModulesToCompileClasspathAction;
3937
import org.gradlex.javamodule.dependencies.internal.utils.ModuleInfo;
4038
import org.gradlex.javamodule.dependencies.tasks.ModuleInfoGeneration;
4139
import org.gradlex.javamodule.dependencies.tasks.ModulePathAnalysis;
@@ -44,7 +42,6 @@
4442
import javax.annotation.Nullable;
4543
import java.io.File;
4644
import java.util.HashMap;
47-
import java.util.List;
4845
import java.util.Map;
4946
import java.util.Optional;
5047
import java.util.stream.Collectors;
@@ -63,8 +60,6 @@ public abstract class JavaModuleDependenciesPlugin implements Plugin<Project> {
6360

6461
private static final String EXTRA_JAVA_MODULE_INFO_PLUGIN_ID = "org.gradlex.extra-java-module-info";
6562

66-
private final Map<File, ModuleInfo> moduleInfo = new HashMap<>();
67-
6863
@Override
6964
public void apply(Project project) {
7065
if (GradleVersion.current().compareTo(GradleVersion.version("7.4")) < 0) {
@@ -91,12 +86,7 @@ private void setupForJavaProject(Project project, JavaModuleDependenciesExtensio
9186

9287
project.getTasks().configureEach(task -> {
9388
if (isJavaCompileTask(task, sourceSet) || isJavadocTask(task, sourceSet)) {
94-
List<String> requiresRuntime =
95-
findModuleInfoInSourceSet(sourceSet, project).get(ModuleInfo.Directive.REQUIRES_RUNTIME);
96-
if (!requiresRuntime.isEmpty()) {
97-
task.doFirst(project.getObjects().newInstance(AddSyntheticModulesToCompileClasspathAction.class,
98-
project.getLayout().getBuildDirectory().dir("tmp/java-module-dependencies/" + task.getName()).get().getAsFile(), requiresRuntime));
99-
}
89+
javaModuleDependencies.doAddRequiresRuntimeSupport(task, sourceSet);
10090
}
10191
});
10292
});
@@ -176,31 +166,13 @@ private void process(ModuleInfo.Directive moduleDirective, String gradleConfigur
176166
}
177167

178168
private void readModuleInfo(ModuleInfo.Directive moduleDirective, SourceSet sourceSet, Project project, Configuration configuration, JavaModuleDependenciesExtension javaModuleDependenciesExtension) {
179-
ModuleInfo moduleInfo = findModuleInfoInSourceSet(sourceSet, project);
169+
ModuleInfo moduleInfo = javaModuleDependenciesExtension.getModuleInfoCache().get(sourceSet);
180170
String ownModuleNamesPrefix = moduleInfo.moduleNamePrefix(project.getName(), sourceSet.getName());
181171
for (String moduleName : moduleInfo.get(moduleDirective)) {
182172
declareDependency(moduleName, ownModuleNamesPrefix, moduleInfo.getFilePath(), project, configuration, javaModuleDependenciesExtension);
183173
}
184174
}
185175

186-
/**
187-
* Returns the module-info.java for the given SourceSet. If the SourceSet has multiple source folders with multiple
188-
* module-info files (which is usually a broken setup) the first file found is returned.
189-
*/
190-
private ModuleInfo findModuleInfoInSourceSet(SourceSet sourceSet, Project project) {
191-
for (File folder : sourceSet.getJava().getSrcDirs()) {
192-
Provider<RegularFile> moduleInfoFile = project.getLayout().file(project.provider(() -> new File(folder, "module-info.java")));
193-
Provider<String> moduleInfoContent = project.getProviders().fileContents(moduleInfoFile).getAsText();
194-
if (moduleInfoContent.isPresent()) {
195-
if (!moduleInfo.containsKey(folder)) {
196-
moduleInfo.put(folder, new ModuleInfo(moduleInfoContent.get(), moduleInfoFile.get().getAsFile()));
197-
}
198-
return moduleInfo.get(folder);
199-
}
200-
}
201-
return ModuleInfo.EMPTY;
202-
}
203-
204176
private void declareDependency(String moduleName, @Nullable String ownModuleNamesPrefix, File moduleInfoFile, Project project, Configuration configuration, JavaModuleDependenciesExtension javaModuleDependencies) {
205177
if (JDKInfo.MODULES.contains(moduleName)) {
206178
// The module is part of the JDK, no dependency required

src/main/java/org/gradlex/javamodule/dependencies/internal/compile/AddSyntheticModulesToCompileClasspathAction.java

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,41 +19,32 @@
1919
import org.gradle.api.Action;
2020
import org.gradle.api.NonNullApi;
2121
import org.gradle.api.Task;
22-
import org.gradle.api.file.ConfigurableFileCollection;
23-
import org.gradle.api.model.ObjectFactory;
22+
import org.gradle.api.file.FileCollection;
2423
import org.gradle.api.tasks.compile.JavaCompile;
2524
import org.gradle.api.tasks.javadoc.Javadoc;
2625
import org.gradlex.javamodule.dependencies.internal.utils.ModuleInfoClassCreator;
2726

2827
import javax.inject.Inject;
2928
import java.io.File;
30-
import java.util.List;
3129

3230
@NonNullApi
3331
public abstract class AddSyntheticModulesToCompileClasspathAction implements Action<Task> {
3432

35-
private final File tmpFolder;
36-
private final List<String> moduleDependencies;
37-
private final ObjectFactory objects;
33+
private final FileCollection syntheticModuleInfoFolders;
3834

3935
@Inject
40-
public AddSyntheticModulesToCompileClasspathAction(File tmpFolder, List<String> moduleDependencies, ObjectFactory objects) {
41-
this.tmpFolder = tmpFolder;
42-
this.moduleDependencies = moduleDependencies;
43-
this.objects = objects;
36+
public AddSyntheticModulesToCompileClasspathAction(FileCollection syntheticModuleInfoFolders) {
37+
this.syntheticModuleInfoFolders = syntheticModuleInfoFolders;
4438
}
4539

4640
@Override
4741
public void execute(Task task) {
48-
if (moduleDependencies.isEmpty()) {
42+
if (syntheticModuleInfoFolders.isEmpty()) {
4943
return;
5044
}
5145

52-
ConfigurableFileCollection syntheticModuleInfoFolders = objects.fileCollection();
53-
for (String moduleName : moduleDependencies) {
54-
File dir = new File(tmpFolder, moduleName + "-synthetic");
55-
ModuleInfoClassCreator.createEmpty(moduleName, dir);
56-
syntheticModuleInfoFolders.from(dir);
46+
for (File moduleFolder : syntheticModuleInfoFolders) {
47+
ModuleInfoClassCreator.createEmpty(moduleFolder);
5748
}
5849

5950
if (task instanceof JavaCompile) {

src/main/java/org/gradlex/javamodule/dependencies/internal/utils/DependencyDeclarationsUtil.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
import static org.gradle.api.attributes.Category.CATEGORY_ATTRIBUTE;
3333
import static org.gradle.api.attributes.Category.LIBRARY;
3434

35-
public class DependencyDeclarationsUtil {
35+
public abstract class DependencyDeclarationsUtil {
3636

3737
public static Provider<List<String>> declaredDependencies(Project project, String configuration) {
3838
ConfigurationContainer configurations = project.getConfigurations();
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2022 the GradleX team.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.gradlex.javamodule.dependencies.internal.utils;
18+
19+
import org.gradle.api.file.ProjectLayout;
20+
import org.gradle.api.file.RegularFile;
21+
import org.gradle.api.provider.Provider;
22+
import org.gradle.api.provider.ProviderFactory;
23+
import org.gradle.api.tasks.SourceSet;
24+
25+
import javax.inject.Inject;
26+
import java.io.File;
27+
import java.util.HashMap;
28+
import java.util.Map;
29+
30+
public abstract class ModuleInfoCache {
31+
32+
private final Map<File, ModuleInfo> moduleInfo = new HashMap<>();
33+
34+
@Inject
35+
protected abstract ProviderFactory getProviders();
36+
37+
@Inject
38+
protected abstract ProjectLayout getLayout();
39+
40+
/**
41+
* Returns the module-info.java for the given SourceSet. If the SourceSet has multiple source folders with multiple
42+
* module-info files (which is usually a broken setup) the first file found is returned.
43+
*/
44+
public ModuleInfo get(SourceSet sourceSet) {
45+
for (File folder : sourceSet.getJava().getSrcDirs()) {
46+
Provider<RegularFile> moduleInfoFile = getLayout().file(getProviders().provider(() -> new File(folder, "module-info.java")));
47+
Provider<String> moduleInfoContent = getProviders().fileContents(moduleInfoFile).getAsText();
48+
if (moduleInfoContent.isPresent()) {
49+
if (!moduleInfo.containsKey(folder)) {
50+
moduleInfo.put(folder, new ModuleInfo(moduleInfoContent.get(), moduleInfoFile.get().getAsFile()));
51+
}
52+
return moduleInfo.get(folder);
53+
}
54+
}
55+
return ModuleInfo.EMPTY;
56+
}
57+
}

src/main/java/org/gradlex/javamodule/dependencies/internal/utils/ModuleInfoClassCreator.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,15 @@
2727
import static org.objectweb.asm.Opcodes.ACC_MODULE;
2828
import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
2929

30-
public class ModuleInfoClassCreator {
30+
public abstract class ModuleInfoClassCreator {
3131

32-
public static void createEmpty(String moduleName, File targetFolder) {
32+
public static void createEmpty(File targetFolder) {
3333
//noinspection ResultOfMethodCallIgnored
3434
targetFolder.mkdirs();
3535

3636
ClassWriter cw = new ClassWriter(0);
3737
cw.visit(53, ACC_MODULE, "module-info", null, null, null);
38-
ModuleVisitor moduleVisitor = cw.visitModule(moduleName, ACC_SYNTHETIC, null);
38+
ModuleVisitor moduleVisitor = cw.visitModule(targetFolder.getName(), ACC_SYNTHETIC, null);
3939
moduleVisitor.visitRequire("java.base", ACC_MANDATED, null);
4040
cw.visitEnd();
4141
try (FileOutputStream s = new FileOutputStream(new File(targetFolder, "module-info.class"))) {

src/main/java/org/gradlex/javamodule/dependencies/internal/utils/ModuleNamingUtil.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import java.util.Arrays;
2222
import java.util.stream.Collectors;
2323

24-
public class ModuleNamingUtil {
24+
public abstract class ModuleNamingUtil {
2525

2626
public static String sourceSetToModuleName(String projectName, String sourceSetName) {
2727
if (sourceSetName.equals(SourceSet.MAIN_SOURCE_SET_NAME)) {

src/main/java/org/gradlex/javamodule/dependencies/internal/utils/TaskConfigurationUtil.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,27 @@
1+
/*
2+
* Copyright 2022 the GradleX team.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
117
package org.gradlex.javamodule.dependencies.internal.utils;
218

319
import org.gradle.api.Task;
420
import org.gradle.api.tasks.SourceSet;
521
import org.gradle.api.tasks.compile.JavaCompile;
622
import org.gradle.api.tasks.javadoc.Javadoc;
723

8-
public class TaskConfigurationUtil {
24+
public abstract class TaskConfigurationUtil {
925

1026
public static boolean isJavaCompileTask(Task task, SourceSet sourceSet) {
1127
return task instanceof JavaCompile && task.getName().equals(sourceSet.getCompileJavaTaskName());

src/test/groovy/org/gradlex/javamodule/dependencies/test/RequiresRuntimeTest.groovy

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,47 @@ class RequiresRuntimeTest extends Specification {
148148
then:
149149
result.task(':app:javadoc').outcome == TaskOutcome.SUCCESS
150150
}
151+
152+
def "can configure additional compile tasks to work with runtime only dependencies"() {
153+
// This is typically needed for whitebox testing
154+
given:
155+
appBuildFile << '''
156+
tasks.compileTestJava {
157+
classpath += sourceSets.main.get().output
158+
159+
val syntheticModules = javaModuleDependencies.addRequiresRuntimeSupport(this, sourceSets.main.get())
160+
161+
options.compilerArgs.add("--module-path")
162+
options.compilerArgs.add(classpath.files.joinToString(":") + ":" + syntheticModules.files.joinToString(":"))
163+
options.compilerArgs.add("--patch-module")
164+
options.compilerArgs.add("org.gradlex.test.app=" + sourceSets.test.get().java.sourceDirectories.first())
165+
166+
}
167+
168+
169+
dependencies.constraints {
170+
javaModuleDependencies {
171+
implementation(gav("org.slf4j", "2.0.3"))
172+
implementation(gav("org.slf4j.simple", "2.0.3"))
173+
}
174+
}
175+
'''
176+
appModuleInfoFile << '''
177+
module org.gradlex.test.app {
178+
requires org.slf4j;
179+
requires /*runtime*/ org.slf4j.simple;
180+
}
181+
'''
182+
file("app/src/test/java/org/gradlex/test/app/MainTest.java") << """
183+
package org.gradlex.test.app;
184+
185+
public class MainTest {}
186+
"""
187+
188+
when:
189+
def result = build()
190+
191+
then:
192+
result.task(":app:compileTestJava").outcome == TaskOutcome.SUCCESS
193+
}
151194
}

0 commit comments

Comments
 (0)