1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one 3 * or more contributor license agreements. See the NOTICE file 4 * distributed with this work for additional information 5 * regarding copyright ownership. The ASF licenses this file 6 * to you under the Apache License, Version 2.0 (the 7 * "License"); you may not use this file except in compliance 8 * with the License. You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, 13 * software distributed under the License is distributed on an 14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 * KIND, either express or implied. See the License for the 16 * specific language governing permissions and limitations 17 * under the License. 18 */ 19 package org.apache.maven.plugin.compiler; 20 21 import javax.lang.model.SourceVersion; 22 import javax.tools.DiagnosticListener; 23 import javax.tools.JavaFileObject; 24 import javax.tools.OptionChecker; 25 26 import java.io.IOException; 27 import java.io.InputStream; 28 import java.lang.module.ModuleDescriptor; 29 import java.nio.file.Files; 30 import java.nio.file.Path; 31 import java.util.List; 32 import java.util.Set; 33 import java.util.TreeMap; 34 import java.util.stream.Stream; 35 36 import org.apache.maven.api.JavaPathType; 37 import org.apache.maven.api.PathScope; 38 import org.apache.maven.api.PathType; 39 import org.apache.maven.api.ProducedArtifact; 40 import org.apache.maven.api.SourceRoot; 41 import org.apache.maven.api.Type; 42 import org.apache.maven.api.annotations.Nonnull; 43 import org.apache.maven.api.annotations.Nullable; 44 import org.apache.maven.api.plugin.MojoException; 45 import org.apache.maven.api.plugin.annotations.Mojo; 46 import org.apache.maven.api.plugin.annotations.Parameter; 47 48 import static org.apache.maven.plugin.compiler.SourceDirectory.CLASS_FILE_SUFFIX; 49 import static org.apache.maven.plugin.compiler.SourceDirectory.JAVA_FILE_SUFFIX; 50 import static org.apache.maven.plugin.compiler.SourceDirectory.MODULE_INFO; 51 52 /** 53 * Compiles application sources. 54 * Each instance shall be used only once, then discarded. 55 * 56 * @author <a href="mailto:jason@maven.org">Jason van Zyl</a> 57 * @author Martin Desruisseaux 58 * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html">javac Command</a> 59 * @since 2.0 60 */ 61 @Mojo(name = "compile", defaultPhase = "compile") 62 public class CompilerMojo extends AbstractCompilerMojo { 63 /** 64 * Set this to {@code true} to bypass compilation of main sources. 65 * Its use is not recommended, but quite convenient on occasion. 66 */ 67 @Parameter(property = "maven.main.skip") 68 protected boolean skipMain; 69 70 /** 71 * Specify where to place generated source files created by annotation processing. 72 * 73 * @since 2.2 74 */ 75 @Parameter(defaultValue = "${project.build.directory}/generated-sources/annotations") 76 protected Path generatedSourcesDirectory; 77 78 /** 79 * A set of inclusion filters for the compiler. 80 */ 81 @Parameter 82 protected Set<String> includes; 83 84 /** 85 * A set of exclusion filters for the compiler. 86 */ 87 @Parameter 88 protected Set<String> excludes; 89 90 /** 91 * A set of exclusion filters for the incremental calculation. 92 * Updated source files, if excluded by this filter, will not cause the project to be rebuilt. 93 * 94 * <h4>Limitation</h4> 95 * In the current implementation, those exclusion filters are applied for added or removed files, 96 * but not yet for removed files. 97 * 98 * @since 3.11 99 */ 100 @Parameter 101 protected Set<String> incrementalExcludes; 102 103 /** 104 * The directory for compiled classes. 105 * 106 * @see #getOutputDirectory() 107 */ 108 @Parameter(defaultValue = "${project.build.outputDirectory}", required = true, readonly = true) 109 protected Path outputDirectory; 110 111 /** 112 * Projects main artifact. 113 */ 114 @Parameter(defaultValue = "${project.mainArtifact}", readonly = true, required = true) 115 protected ProducedArtifact projectArtifact; 116 117 /** 118 * When set to {@code true}, the classes will be placed in {@code META-INF/versions/${release}}. 119 * <p> 120 * <strong>Note:</strong> A jar is only a multi-release jar if {@code META-INF/MANIFEST.MF} contains 121 * {@code Multi-Release: true}. You need to set this by configuring the <a href= 122 * "https://maven.apache.org/plugins/maven-jar-plugin/examples/manifest-customization.html">maven-jar-plugin</a>. 123 * This implies that you cannot test a multi-release jar using the {@link #outputDirectory}. 124 * </p> 125 * 126 * @since 3.7.1 127 * 128 * @deprecated Replaced by specifying the {@code <targetVersion>} value inside a {@code <source>} element. 129 */ 130 @Parameter 131 @Deprecated(since = "4.0.0") 132 protected boolean multiReleaseOutput; 133 134 /** 135 * The file where to dump the command-line when debug is activated or when the compilation failed. 136 * For example, if the value is {@code "javac"}, then the Java compiler can be launched from the 137 * command-line by typing {@code javac @target/javac.args}. 138 * The debug file will contain the compiler options together with the list of source files to compile. 139 * 140 * <p>By default, this debug file is written only if the compilation of main code failed. 141 * The writing of the debug files can be forced by setting the {@link #verbose} flag to {@code true} 142 * or by specifying the {@code --verbose} option to Maven on the command-line.</p> 143 * 144 * @since 3.10.0 145 */ 146 @Parameter(defaultValue = "javac.args") 147 protected String debugFileName; 148 149 /** 150 * Target directory that have been temporarily created as symbolic link before compilation. 151 * This is used as a workaround for the fact that, when compiling a modular project with 152 * all the module-related compiler options, the classes are written in a directory with 153 * the module name. It does not fit in the {@code META-INF/versions/<release>} pattern. 154 * Temporary symbolic link is a workaround for this problem. 155 * 156 * <h4>Example</h4> 157 * When compiling the {@code my.app} module for Java 17, the desired output directory is: 158 * 159 * <blockquote>{@code target/classes/META-INF/versions/17}</blockquote> 160 * 161 * But {@code javac}, when used with the {@code --module-source-path} option, 162 * will write the classes in the following directory: 163 * 164 * <blockquote>{@code target/classes/META-INF/versions/17/my.app}</blockquote> 165 * 166 * We workaround this problem with a symbolic link which redirects {@code 17/my.app} to {@code 17}. 167 * We need to do this only when compiling multi-release project in the old deprecated way. 168 * When using the recommended {@code <sources>} approach, the plugins are designed to work 169 * with the directory layout produced by {@code javac} instead of fighting against it. 170 * 171 * @deprecated For compatibility with the previous way to build multi-release JAR file. 172 * May be removed after we drop support of the old way to do multi-release. 173 */ 174 @Deprecated(since = "4.0.0") 175 private ModuleDirectoryRemover directoryLevelToRemove; 176 177 /** 178 * Creates a new compiler <abbr>MOJO</abbr> for the main code. 179 */ 180 public CompilerMojo() { 181 super(PathScope.MAIN_COMPILE); 182 } 183 184 /** 185 * Runs the Java compiler on the main source code. 186 * If {@link #skipMain} is {@code true}, then this method logs a message and does nothing else. 187 * Otherwise, this method executes the steps described in the method of the parent class. 188 * 189 * @throws MojoException if the compiler cannot be run. 190 */ 191 @Override 192 public void execute() throws MojoException { 193 if (skipMain) { 194 logger.info("Not compiling main sources"); 195 return; 196 } 197 try { 198 super.execute(); 199 } finally { 200 try (ModuleDirectoryRemover r = directoryLevelToRemove) { 201 // Implicit call to directoryLevelToRemove.close(). 202 } catch (IOException e) { 203 throw new CompilationFailureException("I/O error while organizing multi-release classes.", e); 204 } 205 } 206 @SuppressWarnings("LocalVariableHidesMemberVariable") 207 Path outputDirectory = getOutputDirectory(); 208 if (Files.isDirectory(outputDirectory) && projectArtifact != null) { 209 artifactManager.setPath(projectArtifact, outputDirectory); 210 } 211 } 212 213 /** 214 * Parses the parameters declared in the <abbr>MOJO</abbr>. 215 * 216 * @param compiler the tools to use for verifying the validity of options 217 * @return the options after validation 218 */ 219 @Override 220 @SuppressWarnings("deprecation") 221 public Options parseParameters(final OptionChecker compiler) { 222 Options configuration = super.parseParameters(compiler); 223 configuration.addUnchecked(compilerArgs); 224 configuration.addUnchecked(compilerArgument); 225 return configuration; 226 } 227 228 /** 229 * {@return the path where to place generated source files created by annotation processing on the main classes} 230 */ 231 @Nullable 232 @Override 233 protected Path getGeneratedSourcesDirectory() { 234 return generatedSourcesDirectory; 235 } 236 237 /** 238 * {@return the inclusion filters for the compiler, or an empty set for all Java source files} 239 */ 240 @Override 241 protected Set<String> getIncludes() { 242 return (includes != null) ? includes : Set.of(); 243 } 244 245 /** 246 * {@return the exclusion filters for the compiler, or an empty set if none} 247 */ 248 @Override 249 protected Set<String> getExcludes() { 250 return (excludes != null) ? excludes : Set.of(); 251 } 252 253 /** 254 * {@return the exclusion filters for the incremental calculation, or an empty set if none} 255 */ 256 @Override 257 protected Set<String> getIncrementalExcludes() { 258 return (incrementalExcludes != null) ? incrementalExcludes : Set.of(); 259 } 260 261 /** 262 * {@return the destination directory for main class files} 263 * If {@link #multiReleaseOutput} is true <em>(deprecated)</em>, 264 * the output will be in a {@code META-INF/versions} subdirectory. 265 */ 266 @Nonnull 267 @Override 268 protected Path getOutputDirectory() { 269 if (SUPPORT_LEGACY && multiReleaseOutput && release != null) { 270 return SourceDirectory.outputDirectoryForReleases(outputDirectory).resolve(release); 271 } 272 return outputDirectory; 273 } 274 275 /** 276 * {@return the file where to dump the command-line when debug is activated or when the compilation failed} 277 * 278 * @see #debugFileName 279 */ 280 @Nullable 281 @Override 282 protected String getDebugFileName() { 283 return debugFileName; 284 } 285 286 /** 287 * Creates a new task for compiling the main classes. 288 * 289 * @param listener where to send compilation warnings, or {@code null} for the Maven logger 290 * @throws MojoException if this method identifies an invalid parameter in this <abbr>MOJO</abbr> 291 * @return the task to execute for compiling the main code using the configuration in this <abbr>MOJO</abbr> 292 * @throws IOException if an error occurred while creating the output directory or scanning the source directories 293 */ 294 @Override 295 public ToolExecutor createExecutor(DiagnosticListener<? super JavaFileObject> listener) throws IOException { 296 ToolExecutor executor = super.createExecutor(listener); 297 if (SUPPORT_LEGACY && multiReleaseOutput) { 298 addImplicitDependencies(executor); 299 } 300 return executor; 301 } 302 303 /** 304 * {@return whether the project has at least one module-info file} 305 * If no such file is found in the code to be compiled by this <abbr>MOJO</abbr> execution, 306 * then this method searches in the multi-release codes compiled by previous executions. 307 * 308 * @param roots root directories of the sources to compile 309 * @throws IOException if this method needed to read a module descriptor and failed 310 * 311 * @deprecated For compatibility with the previous way to build multi-release JAR file. 312 * May be removed after we drop support of the old way to do multi-release. 313 */ 314 @Override 315 @Deprecated(since = "4.0.0") 316 final boolean hasModuleDeclaration(final List<SourceDirectory> roots) throws IOException { 317 boolean hasModuleDeclaration = super.hasModuleDeclaration(roots); 318 if (SUPPORT_LEGACY && !hasModuleDeclaration && multiReleaseOutput) { 319 String type = project.getPackaging().type().id(); 320 if (!Type.CLASSPATH_JAR.equals(type)) { 321 for (Path p : getOutputDirectoryPerVersion().values()) { 322 p = p.resolve(SourceDirectory.MODULE_INFO + SourceDirectory.CLASS_FILE_SUFFIX); 323 if (Files.exists(p)) { 324 return true; 325 } 326 } 327 } 328 } 329 return hasModuleDeclaration; 330 } 331 332 /** 333 * {@return the output directory of each target Java version} 334 * By convention, {@link SourceVersion#RELEASE_0} stands for the base version. 335 * 336 * @throws IOException if this method needs to walk through directories and that operation failed 337 * 338 * @deprecated For compatibility with the previous way to build multi-release JAR file. 339 * May be removed after we drop support of the old way to do multi-release. 340 */ 341 @Deprecated(since = "4.0.0") 342 private TreeMap<SourceVersion, Path> getOutputDirectoryPerVersion() throws IOException { 343 final Path root = SourceDirectory.outputDirectoryForReleases(outputDirectory); 344 if (Files.notExists(root)) { 345 return null; 346 } 347 final var paths = new TreeMap<SourceVersion, Path>(); 348 Files.walk(root, 1).forEach((path) -> { 349 SourceVersion version; 350 if (path.equals(root)) { 351 path = outputDirectory; 352 version = SourceVersion.RELEASE_0; 353 } else { 354 try { 355 version = SourceVersion.valueOf("RELEASE_" + path.getFileName()); 356 } catch (IllegalArgumentException e) { 357 throw new CompilationFailureException("Invalid version number for " + path, e); 358 } 359 } 360 if (paths.put(version, path) != null) { 361 throw new CompilationFailureException("Duplicated version number for " + path); 362 } 363 }); 364 return paths; 365 } 366 367 /** 368 * Adds the compilation outputs of previous Java releases to the class-path ot module-path. 369 * This method should be invoked only when compiling a multi-release <abbr>JAR</abbr> in the 370 * old deprecated way. 371 * 372 * <p>The {@code executor} argument may be {@code null} if the caller is only interested in the 373 * module name, with no executor to modify. The module name found by this method is specific to 374 * the way that projects are organized when {@link #multiReleaseOutput} is {@code true}.</p> 375 * 376 * @param executor the executor where to add implicit dependencies, or {@code null} if none 377 * @return the module name, or {@code null} if none 378 * @throws IOException if this method needs to walk through directories and that operation failed 379 * 380 * @deprecated For compatibility with the previous way to build multi-release JAR file. 381 * May be removed after we drop support of the old way to do multi-release. 382 */ 383 @Deprecated(since = "4.0.0") 384 private String addImplicitDependencies(final ToolExecutor executor) throws IOException { 385 final TreeMap<SourceVersion, Path> paths = getOutputDirectoryPerVersion(); 386 /* 387 * Search for the module name. If many module-info classes are found, 388 * the most basic one (with lowest Java release number) is selected. 389 */ 390 String moduleName = null; 391 for (Path path : paths.values()) { 392 path = path.resolve(MODULE_INFO + CLASS_FILE_SUFFIX); 393 if (Files.exists(path)) { 394 try (InputStream in = Files.newInputStream(path)) { 395 moduleName = ModuleDescriptor.read(in).name(); 396 } 397 break; 398 } 399 } 400 /* 401 * If no module name was found in the classes compiled for previous Java releases, 402 * search in the source files for the Java release of the current compilation unit. 403 */ 404 if (moduleName == null) { 405 final Stream<Path> sourceDirectories; 406 if (executor != null) { 407 sourceDirectories = executor.sourceDirectories.stream().map(dir -> dir.root); 408 } else if (compileSourceRoots == null || compileSourceRoots.isEmpty()) { 409 sourceDirectories = getSourceRoots(compileScope.projectScope()).map(SourceRoot::directory); 410 } else { 411 sourceDirectories = compileSourceRoots.stream().map(Path::of); 412 } 413 for (Path root : sourceDirectories.toList()) { 414 moduleName = parseModuleInfoName(root.resolve(MODULE_INFO + JAVA_FILE_SUFFIX)); 415 if (moduleName != null) { 416 break; 417 } 418 } 419 } 420 if (executor != null) { 421 /* 422 * Add previous versions as dependencies on the class-path or module-path, depending on whether 423 * the project is modular. Each path should be on either the class-path or module-path, but not 424 * both. If a path for a modular project seems needed on the class-path, it may be a sign that 425 * other options are not used correctly (e.g., `--source-path` versus `--module-source-path`). 426 */ 427 PathType type = JavaPathType.CLASSES; 428 if (moduleName != null) { 429 type = JavaPathType.patchModule(moduleName); 430 directoryLevelToRemove = ModuleDirectoryRemover.create(executor.outputDirectory, moduleName); 431 } 432 if (!paths.isEmpty()) { 433 executor.dependencies(type).addAll(paths.descendingMap().values()); 434 } 435 } 436 return moduleName; 437 } 438 439 /** 440 * {@return the module name in a previous execution of the compiler plugin, or {@code null} if none} 441 * 442 * @deprecated For compatibility with the previous way to build multi-release JAR file. 443 * May be removed after we drop support of the old way to do multi-release. 444 */ 445 @Override 446 @Deprecated(since = "4.0.0") 447 final String moduleOfPreviousExecution() throws IOException { 448 if (SUPPORT_LEGACY && multiReleaseOutput) { 449 return addImplicitDependencies(null); 450 } 451 return super.moduleOfPreviousExecution(); 452 } 453 }