|
22 | 22 | import javax.tools.JavaCompiler; |
23 | 23 | import javax.tools.JavaFileObject; |
24 | 24 |
|
| 25 | +import java.io.BufferedReader; |
| 26 | +import java.io.BufferedWriter; |
25 | 27 | import java.io.IOException; |
26 | 28 | import java.io.InputStream; |
27 | 29 | import java.io.Writer; |
28 | 30 | import java.lang.module.ModuleDescriptor; |
29 | 31 | import java.nio.file.Files; |
30 | 32 | import java.nio.file.Path; |
31 | | -import java.util.HashSet; |
32 | 33 | import java.util.LinkedHashMap; |
33 | 34 | import java.util.LinkedHashSet; |
34 | 35 | import java.util.List; |
35 | 36 | import java.util.Map; |
36 | 37 | import java.util.Set; |
37 | | -import java.util.StringJoiner; |
38 | 38 |
|
39 | | -import org.apache.maven.api.Dependency; |
40 | 39 | import org.apache.maven.api.JavaPathType; |
41 | 40 | import org.apache.maven.api.PathType; |
42 | 41 | import org.apache.maven.api.ProjectScope; |
@@ -69,15 +68,21 @@ class ToolExecutorForTest extends ToolExecutor { |
69 | 68 | * in which case the main classes are placed on the class path, but this is deprecated. |
70 | 69 | * This flag may be removed in a future version if we remove support of this practice. |
71 | 70 | * |
| 71 | + * @deprecated Use {@code "claspath-jar"} dependency type instead, and avoid {@code module-info.java} in tests. |
| 72 | + * |
72 | 73 | * @see TestCompilerMojo#useModulePath |
73 | 74 | */ |
| 75 | + @Deprecated(since = "4.0.0") |
74 | 76 | private final boolean useModulePath; |
75 | 77 |
|
76 | 78 | /** |
77 | 79 | * Whether a {@code module-info.java} file is defined in the test sources. |
78 | 80 | * In such case, it has precedence over the {@code module-info.java} in main sources. |
79 | 81 | * This is defined for compatibility with Maven 3, but not recommended. |
| 82 | + * |
| 83 | + * @deprecated Avoid {@code module-info.java} in tests. |
80 | 84 | */ |
| 85 | + @Deprecated(since = "4.0.0") |
81 | 86 | private final boolean hasTestModuleInfo; |
82 | 87 |
|
83 | 88 | /** |
@@ -232,92 +237,95 @@ final String inferModuleNameIfMissing(String moduleName) throws IOException { |
232 | 237 | } |
233 | 238 |
|
234 | 239 | /** |
235 | | - * Generates the {@code --add-modules} and {@code --add-reads} options for the dependencies that are not |
236 | | - * in the main compilation. This method is invoked only if {@code hasModuleDeclaration} is {@code true}. |
| 240 | + * Completes the given configuration with module options the first time that this method is invoked. |
| 241 | + * If at least one {@value ModuleInfoPatch#FILENAME} file is found in a root directory of test sources, |
| 242 | + * then these files are parsed and the options that they declare are added to the given configuration. |
| 243 | + * Otherwise, if {@link #hasModuleDeclaration} is {@code true}, then this method generates the |
| 244 | + * {@code --add-modules} and {@code --add-reads} options for dependencies that are not in the main compilation. |
| 245 | + * If this method is invoked more than once, all invocations after the first one have no effect. |
237 | 246 | * |
238 | | - * @param dependencyResolution the project dependencies |
239 | 247 | * @param configuration where to add the options |
240 | | - * @throws IOException if the module information of a dependency cannot be read |
| 248 | + * @throws IOException if the module information of a dependency or the module-info patch cannot be read |
241 | 249 | */ |
242 | 250 | @SuppressWarnings({"checkstyle:MissingSwitchDefault", "fallthrough"}) |
243 | 251 | private void addModuleOptions(final Options configuration) throws IOException { |
244 | 252 | if (addedModuleOptions) { |
245 | 253 | return; |
246 | 254 | } |
247 | 255 | addedModuleOptions = true; |
248 | | - if (!hasModuleDeclaration || dependencyResolution == null) { |
249 | | - return; |
250 | | - } |
251 | | - if (SUPPORT_LEGACY && useModulePath && hasTestModuleInfo) { |
252 | | - /* |
253 | | - * Do not add any `--add-reads` parameters. The developers should put |
254 | | - * everything needed in the `module-info`, including test dependencies. |
255 | | - */ |
256 | | - return; |
257 | | - } |
258 | | - final var done = new HashSet<String>(); // Added modules and their dependencies. |
259 | | - final var addModules = new StringJoiner(","); |
260 | | - StringJoiner addReads = null; |
261 | | - boolean hasUnnamed = false; |
262 | | - for (Map.Entry<Dependency, Path> entry : |
263 | | - dependencyResolution.getDependencies().entrySet()) { |
264 | | - boolean compile = false; |
265 | | - switch (entry.getKey().getScope()) { |
266 | | - case TEST: |
267 | | - case TEST_ONLY: |
268 | | - compile = true; |
269 | | - // Fall through |
270 | | - case TEST_RUNTIME: |
271 | | - if (compile) { |
272 | | - // Needs to be initialized even if `name` is null. |
273 | | - if (addReads == null) { |
274 | | - addReads = new StringJoiner(","); |
275 | | - } |
276 | | - } |
277 | | - Path path = entry.getValue(); |
278 | | - String name = dependencyResolution.getModuleName(path).orElse(null); |
279 | | - if (name == null) { |
280 | | - hasUnnamed = true; |
281 | | - } else if (done.add(name)) { |
282 | | - addModules.add(name); |
283 | | - if (compile) { |
284 | | - addReads.add(name); |
285 | | - } |
286 | | - /* |
287 | | - * For making the options simpler, we do not add `--add-modules` or `--add-reads` |
288 | | - * options for modules that are required by a module that we already added. This |
289 | | - * simplification is not necessary, but makes the command-line easier to read. |
290 | | - */ |
291 | | - dependencyResolution.getModuleDescriptor(path).ifPresent((descriptor) -> { |
292 | | - for (ModuleDescriptor.Requires r : descriptor.requires()) { |
293 | | - done.add(r.name()); |
294 | | - } |
295 | | - }); |
| 256 | + ModuleInfoPatch info = null; |
| 257 | + ModuleInfoPatch defaultInfo = null; |
| 258 | + final var patches = new LinkedHashMap<String, ModuleInfoPatch>(); |
| 259 | + for (SourceDirectory source : sourceDirectories) { |
| 260 | + Path file = source.root.resolve(ModuleInfoPatch.FILENAME); |
| 261 | + String module; |
| 262 | + if (Files.notExists(file)) { |
| 263 | + if (SUPPORT_LEGACY && useModulePath && hasTestModuleInfo && hasModuleDeclaration) { |
| 264 | + /* |
| 265 | + * Do not add any `--add-reads` parameters. The developers should put |
| 266 | + * everything needed in the `module-info`, including test dependencies. |
| 267 | + */ |
| 268 | + continue; |
| 269 | + } |
| 270 | + /* |
| 271 | + * No `patch-module-info` file. Generate a default module patch instance for the |
| 272 | + * `--add-modules TEST-MODULE-PATH` and `--add-reads TEST-MODULE-PATH` options. |
| 273 | + * We generate that patch only for the first module. If there is more modules |
| 274 | + * without `patch-module-info`, we will copy the `defaultInfo` instance. |
| 275 | + */ |
| 276 | + module = source.moduleName; |
| 277 | + if (module == null) { |
| 278 | + module = getMainModuleName(); |
| 279 | + if (module.isEmpty()) { |
| 280 | + continue; |
296 | 281 | } |
297 | | - break; |
| 282 | + } |
| 283 | + if (defaultInfo != null) { |
| 284 | + patches.putIfAbsent(module, null); // Remember that we will need to compute a value later. |
| 285 | + continue; |
| 286 | + } |
| 287 | + defaultInfo = new ModuleInfoPatch(module, info); |
| 288 | + defaultInfo.setToDefaults(); |
| 289 | + info = defaultInfo; |
| 290 | + } else { |
| 291 | + info = new ModuleInfoPatch(getMainModuleName(), info); |
| 292 | + try (BufferedReader reader = Files.newBufferedReader(file)) { |
| 293 | + info.load(reader); |
| 294 | + } |
| 295 | + module = info.getModuleName(); |
298 | 296 | } |
299 | | - } |
300 | | - if (!done.isEmpty()) { |
301 | | - configuration.addIfNonBlank("--add-modules", addModules.toString()); |
302 | | - } |
303 | | - if (addReads != null) { |
304 | | - if (hasUnnamed) { |
305 | | - addReads.add("ALL-UNNAMED"); |
| 297 | + if (patches.put(module, info) != null) { |
| 298 | + throw new ModuleInfoPatchException("\"module-info-patch " + module + "\" is defined more than once."); |
306 | 299 | } |
307 | | - String reads = addReads.toString(); |
308 | | - addReads(configuration, getMainModuleName(), reads); |
309 | | - for (SourceDirectory root : sourceDirectories) { |
310 | | - addReads(configuration, root.moduleName, reads); |
| 300 | + } |
| 301 | + /* |
| 302 | + * Replace all occurrences of `TEST-MODULE-PATH` by the actual dependency paths. |
| 303 | + * Add `--add-modules` and `--add-reads` options with default values equivalent to |
| 304 | + * `TEST-MODULE-PATH` for every module that do not have a `module-info-patch` file. |
| 305 | + */ |
| 306 | + for (Map.Entry<String, ModuleInfoPatch> entry : patches.entrySet()) { |
| 307 | + info = entry.getValue(); |
| 308 | + if (info != null) { |
| 309 | + info.replaceProjectModules(sourceDirectories); |
| 310 | + info.replaceTestModulePath(dependencyResolution); |
| 311 | + } else { |
| 312 | + // `defaultInfo` cannot be null if this `info` value is null. |
| 313 | + entry.setValue(defaultInfo.patchWithSameReads(entry.getKey())); |
311 | 314 | } |
312 | 315 | } |
313 | | - } |
314 | | - |
315 | | - /** |
316 | | - * Adds an {@code --add-reads} compiler option if the given module name is non-null and non-blank. |
317 | | - */ |
318 | | - private static void addReads(Options configuration, String moduleName, String reads) { |
319 | | - if (moduleName != null && !moduleName.isBlank()) { |
320 | | - configuration.addIfNonBlank("--add-reads", moduleName + '=' + reads); |
| 316 | + /* |
| 317 | + * Write the runtime dependencies in the `META-INF/maven/module-info-patch.args` file. |
| 318 | + * Note that we unconditionally write in the root output directory, not in the module directory, |
| 319 | + * because a single option file applies to all modules. |
| 320 | + */ |
| 321 | + if (!patches.isEmpty()) { |
| 322 | + Path directory = // TODO: replace by Path.resolve(String, String...) with JDK22. |
| 323 | + Files.createDirectories(outputDirectory.resolve("META-INF").resolve("maven")); |
| 324 | + try (BufferedWriter out = Files.newBufferedWriter(directory.resolve("module-info-patch.args"))) { |
| 325 | + for (ModuleInfoPatch m : patches.values()) { |
| 326 | + m.writeTo(configuration, out); |
| 327 | + } |
| 328 | + } |
321 | 329 | } |
322 | 330 | } |
323 | 331 |
|
|
0 commit comments