Skip to content

Commit 9577b7c

Browse files
committed
Add synthetic module-infos for runtime dependencies to compile classpath
To keep code with 'requires /*runtime*/' directives compiling, we add a synthetic (empty) module descriptor for all of these dependencies directly to the classpath of the corresponding JavaCompile task.
1 parent 2c88fe1 commit 9577b7c

File tree

7 files changed

+205
-27
lines changed

7 files changed

+205
-27
lines changed

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

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,6 @@
1616

1717
package org.gradlex.javamodule.dependencies;
1818

19-
import org.gradlex.javamodule.dependencies.internal.bridges.ExtraJavaModuleInfoBridge;
20-
import org.gradlex.javamodule.dependencies.internal.utils.ModuleInfo;
21-
import org.gradlex.javamodule.dependencies.tasks.ModuleInfoGeneration;
22-
import org.gradlex.javamodule.dependencies.tasks.ModuleVersionRecommendation;
23-
import org.gradlex.javamodule.dependencies.tasks.ModulePathAnalysis;
2419
import org.gradle.api.GradleException;
2520
import org.gradle.api.NonNullApi;
2621
import org.gradle.api.Plugin;
@@ -38,11 +33,19 @@
3833
import org.gradle.api.tasks.SourceSet;
3934
import org.gradle.api.tasks.SourceSetContainer;
4035
import org.gradle.api.tasks.TaskProvider;
36+
import org.gradle.api.tasks.compile.JavaCompile;
4137
import org.gradle.util.GradleVersion;
38+
import org.gradlex.javamodule.dependencies.internal.bridges.ExtraJavaModuleInfoBridge;
39+
import org.gradlex.javamodule.dependencies.internal.compile.AddSyntheticModulesToCompileClasspathAction;
40+
import org.gradlex.javamodule.dependencies.internal.utils.ModuleInfo;
41+
import org.gradlex.javamodule.dependencies.tasks.ModuleInfoGeneration;
42+
import org.gradlex.javamodule.dependencies.tasks.ModulePathAnalysis;
43+
import org.gradlex.javamodule.dependencies.tasks.ModuleVersionRecommendation;
4244

4345
import javax.annotation.Nullable;
4446
import java.io.File;
4547
import java.util.HashMap;
48+
import java.util.List;
4649
import java.util.Map;
4750
import java.util.Optional;
4851
import java.util.stream.Collectors;
@@ -84,6 +87,15 @@ private void setupForJavaProject(Project project, JavaModuleDependenciesExtensio
8487
process(ModuleInfo.Directive.REQUIRES_TRANSITIVE, sourceSet.getApiConfigurationName(), sourceSet, project, javaModuleDependencies);
8588
process(ModuleInfo.Directive.REQUIRES_STATIC_TRANSITIVE, sourceSet.getCompileOnlyApiConfigurationName(), sourceSet, project, javaModuleDependencies);
8689
process(ModuleInfo.Directive.REQUIRES_RUNTIME, sourceSet.getRuntimeOnlyConfigurationName(), sourceSet, project, javaModuleDependencies);
90+
91+
project.getTasks().named(sourceSet.getCompileJavaTaskName(), JavaCompile.class, javaCompile -> {
92+
ModuleInfo moduleInfo = findModuleInfoInSourceSet(sourceSet, project);
93+
List<String> requiresRuntime = moduleInfo.get(ModuleInfo.Directive.REQUIRES_RUNTIME);
94+
if (!requiresRuntime.isEmpty()) {
95+
javaCompile.doFirst(project.getObjects().newInstance(AddSyntheticModulesToCompileClasspathAction.class,
96+
project.getLayout().getBuildDirectory().dir("tmp").get().getAsFile(), requiresRuntime));
97+
}
98+
});
8799
});
88100
setupReportTasks(project, javaModuleDependencies);
89101
setupMigrationTasks(project, javaModuleDependencies);
@@ -150,34 +162,43 @@ private void setupMigrationTasks(Project project, JavaModuleDependenciesExtensio
150162
private void process(ModuleInfo.Directive moduleDirective, String gradleConfiguration, SourceSet sourceSet, Project project, JavaModuleDependenciesExtension javaModuleDependenciesExtension) {
151163
Configuration conf = project.getConfigurations().findByName(gradleConfiguration);
152164
if (conf != null) {
153-
conf.withDependencies(d -> findAndReadModuleInfo(moduleDirective, sourceSet, project, conf, javaModuleDependenciesExtension));
165+
conf.withDependencies(d -> readModuleInfo(moduleDirective, sourceSet, project, conf, javaModuleDependenciesExtension));
154166
} else {
155167
project.getConfigurations().whenObjectAdded(lateAddedConf -> {
156168
if (gradleConfiguration.equals(lateAddedConf.getName())) {
157-
lateAddedConf.withDependencies(d -> findAndReadModuleInfo(moduleDirective, sourceSet, project, lateAddedConf, javaModuleDependenciesExtension));
169+
lateAddedConf.withDependencies(d -> readModuleInfo(moduleDirective, sourceSet, project, lateAddedConf, javaModuleDependenciesExtension));
158170
}
159171
});
160172
}
161173
}
162174

163-
private void findAndReadModuleInfo(ModuleInfo.Directive moduleDirective, SourceSet sourceSet, Project project, Configuration configuration, JavaModuleDependenciesExtension javaModuleDependenciesExtension) {
175+
private void readModuleInfo(ModuleInfo.Directive moduleDirective, SourceSet sourceSet, Project project, Configuration configuration, JavaModuleDependenciesExtension javaModuleDependenciesExtension) {
176+
ModuleInfo moduleInfo = findModuleInfoInSourceSet(sourceSet, project);
177+
String ownModuleNamesPrefix = moduleInfo.moduleNamePrefix(project.getName(), sourceSet.getName());
178+
for (String moduleName : moduleInfo.get(moduleDirective)) {
179+
declareDependency(moduleName, ownModuleNamesPrefix, moduleInfo.getFilePath(), project, configuration, javaModuleDependenciesExtension);
180+
}
181+
}
182+
183+
/**
184+
* Returns the module-info.java for the given SourceSet. If the SourceSet has multiple source folders with multiple
185+
* module-info files (which is usually a broken setup) the first file found is returned.
186+
*/
187+
private ModuleInfo findModuleInfoInSourceSet(SourceSet sourceSet, Project project) {
164188
for (File folder : sourceSet.getJava().getSrcDirs()) {
165189
Provider<RegularFile> moduleInfoFile = project.getLayout().file(project.provider(() -> new File(folder, "module-info.java")));
166190
Provider<String> moduleInfoContent = project.getProviders().fileContents(moduleInfoFile).getAsText();
167191
if (moduleInfoContent.isPresent()) {
168-
if (!this.moduleInfo.containsKey(folder)) {
169-
this.moduleInfo.put(folder, new ModuleInfo(moduleInfoContent.get()));
170-
}
171-
ModuleInfo moduleInfo = this.moduleInfo.get(folder);
172-
String ownModuleNamesPrefix = moduleInfo.moduleNamePrefix(project.getName(), sourceSet.getName());
173-
for (String moduleName : moduleInfo.get(moduleDirective)) {
174-
declareDependency(moduleName, ownModuleNamesPrefix, moduleInfoFile, project, configuration, javaModuleDependenciesExtension);
192+
if (!moduleInfo.containsKey(folder)) {
193+
moduleInfo.put(folder, new ModuleInfo(moduleInfoContent.get(), moduleInfoFile.get().getAsFile()));
175194
}
195+
return moduleInfo.get(folder);
176196
}
177197
}
198+
return ModuleInfo.EMPTY;
178199
}
179200

180-
private void declareDependency(String moduleName, @Nullable String ownModuleNamesPrefix, Provider<RegularFile> moduleInfoFile, Project project, Configuration configuration, JavaModuleDependenciesExtension javaModuleDependencies) {
201+
private void declareDependency(String moduleName, @Nullable String ownModuleNamesPrefix, File moduleInfoFile, Project project, Configuration configuration, JavaModuleDependenciesExtension javaModuleDependencies) {
181202
if (JDKInfo.MODULES.contains(moduleName)) {
182203
// The module is part of the JDK, no dependency required
183204
return;
@@ -241,17 +262,17 @@ private void declareDependency(String moduleName, @Nullable String ownModuleName
241262
}
242263
}
243264

244-
private void warnVersionMissing(String moduleName, Map<String, Object> ga, Provider<RegularFile> moduleInfoFile, Project project, JavaModuleDependenciesExtension javaModuleDependencies) {
265+
private void warnVersionMissing(String moduleName, Map<String, Object> ga, File moduleInfoFile, Project project, JavaModuleDependenciesExtension javaModuleDependencies) {
245266
if (javaModuleDependencies.getWarnForMissingVersions().get()) {
246267
project.getLogger().warn("[WARN] [Java Module Dependencies] No version defined in catalog - " + ga.get(GAV.GROUP) + ":" + ga.get(GAV.ARTIFACT) + " - "
247268
+ moduleDebugInfo(moduleName.replace('.', '_'), moduleInfoFile, project.getRootDir()));
248269
}
249270
}
250271

251-
private String moduleDebugInfo(String moduleName, Provider<RegularFile> moduleInfoFile, File rootDir) {
272+
private String moduleDebugInfo(String moduleName, File moduleInfoFile, File rootDir) {
252273
return moduleName
253274
+ " (required in "
254-
+ moduleInfoFile.get().getAsFile().getAbsolutePath().substring(rootDir.getAbsolutePath().length() + 1)
275+
+ moduleInfoFile.getAbsolutePath().substring(rootDir.getAbsolutePath().length() + 1)
255276
+ ")";
256277
}
257278

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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.compile;
18+
19+
import org.gradle.api.Action;
20+
import org.gradle.api.NonNullApi;
21+
import org.gradle.api.Task;
22+
import org.gradle.api.file.ConfigurableFileCollection;
23+
import org.gradle.api.model.ObjectFactory;
24+
import org.gradle.api.tasks.compile.JavaCompile;
25+
import org.gradlex.javamodule.dependencies.internal.utils.ModuleInfoClassCreator;
26+
27+
import javax.inject.Inject;
28+
import java.io.File;
29+
import java.util.List;
30+
31+
@NonNullApi
32+
public abstract class AddSyntheticModulesToCompileClasspathAction implements Action<Task> {
33+
34+
private final File tmpFolder;
35+
private final List<String> moduleDependencies;
36+
private final ObjectFactory objects;
37+
38+
@Inject
39+
public AddSyntheticModulesToCompileClasspathAction(File tmpFolder, List<String> moduleDependencies, ObjectFactory objects) {
40+
this.tmpFolder = tmpFolder;
41+
this.moduleDependencies = moduleDependencies;
42+
this.objects = objects;
43+
}
44+
45+
@Override
46+
public void execute(Task task) {
47+
if (moduleDependencies.isEmpty()) {
48+
return;
49+
}
50+
51+
JavaCompile javaCompile = (JavaCompile) task;
52+
53+
ConfigurableFileCollection syntheticModuleInfoFolders = objects.fileCollection();
54+
for (String moduleName : moduleDependencies) {
55+
File dir = new File(tmpFolder, "java-module-dependencies/" + moduleName + "-synthetic");
56+
ModuleInfoClassCreator.createEmpty(moduleName, dir);
57+
syntheticModuleInfoFolders.from(dir);
58+
}
59+
60+
javaCompile.setClasspath(javaCompile.getClasspath().plus(syntheticModuleInfoFolders));
61+
}
62+
}

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.gradlex.javamodule.dependencies.internal.utils;
1818

1919
import javax.annotation.Nullable;
20+
import java.io.File;
2021
import java.util.ArrayList;
2122
import java.util.Arrays;
2223
import java.util.Collections;
@@ -41,14 +42,19 @@ public String literal() {
4142

4243
public static final String RUNTIME_KEYWORD = "/*runtime*/";
4344

44-
private String moduleName;
45+
public static final ModuleInfo EMPTY = new ModuleInfo("", new File(""));
46+
47+
private String moduleName = "";
4548
private final List<String> requires = new ArrayList<>();
4649
private final List<String> requiresTransitive = new ArrayList<>();
4750
private final List<String> requiresStatic = new ArrayList<>();
4851
private final List<String> requiresStaticTransitive = new ArrayList<>();
4952
private final List<String> requiresRuntime = new ArrayList<>();
5053

51-
public ModuleInfo(String moduleInfoFileContent) {
54+
private final File filePath;
55+
56+
public ModuleInfo(String moduleInfoFileContent, File filePath) {
57+
this.filePath = filePath;
5258
boolean insideComment = false;
5359
for(String line: moduleInfoFileContent.split("\n")) {
5460
insideComment = parse(line, insideComment);
@@ -90,6 +96,10 @@ public String moduleNamePrefix(String projectName, String sourceSetName) {
9096
return null;
9197
}
9298

99+
public File getFilePath() {
100+
return filePath;
101+
}
102+
93103
/**
94104
* @return true, if we are inside a multi-line comment after this line
95105
*/
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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.objectweb.asm.ClassWriter;
20+
import org.objectweb.asm.ModuleVisitor;
21+
22+
import java.io.File;
23+
import java.io.FileOutputStream;
24+
import java.io.IOException;
25+
26+
import static org.objectweb.asm.Opcodes.ACC_MANDATED;
27+
import static org.objectweb.asm.Opcodes.ACC_MODULE;
28+
import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
29+
30+
public class ModuleInfoClassCreator {
31+
32+
public static void createEmpty(String moduleName, File targetFolder) {
33+
//noinspection ResultOfMethodCallIgnored
34+
targetFolder.mkdirs();
35+
36+
ClassWriter cw = new ClassWriter(0);
37+
cw.visit(53, ACC_MODULE, "module-info", null, null, null);
38+
ModuleVisitor moduleVisitor = cw.visitModule(moduleName, ACC_SYNTHETIC, null);
39+
moduleVisitor.visitRequire("java.base", ACC_MANDATED, null);
40+
cw.visitEnd();
41+
try (FileOutputStream s = new FileOutputStream(new File(targetFolder, "module-info.class"))) {
42+
s.write(cw.toByteArray());
43+
} catch (IOException e) {
44+
throw new RuntimeException(e);
45+
}
46+
}
47+
}

src/main/java/org/gradlex/javamodule/dependencies/tasks/ModulePathAnalysis.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public void report() throws IOException {
9090
if (file.exists()) {
9191
try(Stream<String> lines = Files.lines(file.toPath())) {
9292
String fileContent = lines.collect(Collectors.joining("\n"));
93-
ownModuleNamesPrefix = new ModuleInfo(fileContent).moduleNamePrefix(projectName, main.getName());
93+
ownModuleNamesPrefix = new ModuleInfo(fileContent, file).moduleNamePrefix(projectName, main.getName());
9494
}
9595
break;
9696
}

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class ModuleInfoParseTest extends Specification {
1818
// requires com.bla.blub;
1919
requires transitive foo.bar.la;
2020
}
21-
''')
21+
''', new File(''))
2222

2323
expect:
2424
moduleInfo.moduleNamePrefix("thing", "main") == "some"
@@ -35,7 +35,7 @@ class ModuleInfoParseTest extends Specification {
3535
module some.thing { // module some.thing.else
3636
requires transitive foo.bar.la;
3737
}
38-
''')
38+
''', new File(''))
3939

4040
expect:
4141
moduleInfo.moduleNamePrefix("thing", "main") == "some"
@@ -55,7 +55,7 @@ class ModuleInfoParseTest extends Specification {
5555
*/
5656
requires static foo.bar.la;
5757
}
58-
''')
58+
''', new File(''))
5959

6060
expect:
6161
moduleInfo.get(REQUIRES) == []
@@ -75,7 +75,7 @@ class ModuleInfoParseTest extends Specification {
7575
requires only.a.comment
7676
*/
7777
}
78-
''')
78+
''', new File(''))
7979

8080
expect:
8181
moduleInfo.get(REQUIRES) == ["foo.bar.li"]
@@ -91,7 +91,7 @@ class ModuleInfoParseTest extends Specification {
9191
module some.thing {
9292
requires /*runtime*/ foo.bar.lo;
9393
}
94-
''')
94+
''', new File(''))
9595

9696
expect:
9797
moduleInfo.get(REQUIRES) == []

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,42 @@ class RequiresRuntimeTest extends Specification {
7777
then:
7878
result.output.contains("[main] INFO org.gradlex.test.app.Main - Running application...")
7979
}
80+
81+
def "runtime only dependencies are not visible at compile time"() {
82+
given:
83+
appBuildFile << '''
84+
dependencies.constraints {
85+
javaModuleDependencies {
86+
implementation(gav("org.slf4j", "2.0.3"))
87+
}
88+
}
89+
'''
90+
appModuleInfoFile << '''
91+
module org.gradlex.test.app {
92+
requires /*runtime*/ org.slf4j;
93+
94+
exports org.gradlex.test.app;
95+
}
96+
'''
97+
file("app/src/main/java/org/gradlex/test/app/Main.java") << """
98+
package org.gradlex.test.app;
99+
100+
import org.slf4j.Logger;
101+
import org.slf4j.LoggerFactory;
102+
103+
public class Main {
104+
private static final Logger LOGGER = LoggerFactory.getLogger(Main.class);
105+
106+
public static void main(String[] args) {
107+
LOGGER.info("Running application...");
108+
}
109+
}
110+
"""
111+
112+
when:
113+
def result = fail()
114+
115+
then:
116+
result.output.contains("error: package org.slf4j does not exist")
117+
}
80118
}

0 commit comments

Comments
 (0)