| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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.apache.maven.plugin.compiler; |
| |
| import javax.lang.model.SourceVersion; |
| import javax.tools.DiagnosticListener; |
| import javax.tools.JavaFileObject; |
| import javax.tools.OptionChecker; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.lang.module.ModuleDescriptor; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.TreeMap; |
| import java.util.stream.Stream; |
| |
| import org.apache.maven.api.JavaPathType; |
| import org.apache.maven.api.PathScope; |
| import org.apache.maven.api.PathType; |
| import org.apache.maven.api.ProducedArtifact; |
| import org.apache.maven.api.SourceRoot; |
| import org.apache.maven.api.Type; |
| import org.apache.maven.api.annotations.Nonnull; |
| import org.apache.maven.api.annotations.Nullable; |
| import org.apache.maven.api.plugin.MojoException; |
| import org.apache.maven.api.plugin.annotations.Mojo; |
| import org.apache.maven.api.plugin.annotations.Parameter; |
| |
| import static org.apache.maven.plugin.compiler.SourceDirectory.CLASS_FILE_SUFFIX; |
| import static org.apache.maven.plugin.compiler.SourceDirectory.JAVA_FILE_SUFFIX; |
| import static org.apache.maven.plugin.compiler.SourceDirectory.MODULE_INFO; |
| |
| /** |
| * Compiles application sources. |
| * Each instance shall be used only once, then discarded. |
| * |
| * @author <a href="mailto:jason@maven.org">Jason van Zyl</a> |
| * @author Martin Desruisseaux |
| * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html">javac Command</a> |
| * @since 2.0 |
| */ |
| @Mojo(name = "compile", defaultPhase = "compile") |
| public class CompilerMojo extends AbstractCompilerMojo { |
| /** |
| * Set this to {@code true} to bypass compilation of main sources. |
| * Its use is not recommended, but quite convenient on occasion. |
| */ |
| @Parameter(property = "maven.main.skip") |
| protected boolean skipMain; |
| |
| /** |
| * Specify where to place generated source files created by annotation processing. |
| * |
| * @since 2.2 |
| */ |
| @Parameter(defaultValue = "${project.build.directory}/generated-sources/annotations") |
| protected Path generatedSourcesDirectory; |
| |
| /** |
| * A set of inclusion filters for the compiler. |
| */ |
| @Parameter |
| protected Set<String> includes; |
| |
| /** |
| * A set of exclusion filters for the compiler. |
| */ |
| @Parameter |
| protected Set<String> excludes; |
| |
| /** |
| * A set of exclusion filters for the incremental calculation. |
| * Updated source files, if excluded by this filter, will not cause the project to be rebuilt. |
| * |
| * <h4>Limitation</h4> |
| * In the current implementation, those exclusion filters are applied for added or removed files, |
| * but not yet for removed files. |
| * |
| * @since 3.11 |
| */ |
| @Parameter |
| protected Set<String> incrementalExcludes; |
| |
| /** |
| * The directory for compiled classes. |
| * |
| * @see #getOutputDirectory() |
| */ |
| @Parameter(defaultValue = "${project.build.outputDirectory}", required = true, readonly = true) |
| protected Path outputDirectory; |
| |
| /** |
| * Projects main artifact. |
| */ |
| @Parameter(defaultValue = "${project.mainArtifact}", readonly = true, required = true) |
| protected ProducedArtifact projectArtifact; |
| |
| /** |
| * When set to {@code true}, the classes will be placed in {@code META-INF/versions/${release}}. |
| * <p> |
| * <strong>Note:</strong> A jar is only a multi-release jar if {@code META-INF/MANIFEST.MF} contains |
| * {@code Multi-Release: true}. You need to set this by configuring the <a href= |
| * "https://maven.apache.org/plugins/maven-jar-plugin/examples/manifest-customization.html">maven-jar-plugin</a>. |
| * This implies that you cannot test a multi-release jar using the {@link #outputDirectory}. |
| * </p> |
| * |
| * @since 3.7.1 |
| * |
| * @deprecated Replaced by specifying the {@code <targetVersion>} value inside a {@code <source>} element. |
| */ |
| @Parameter |
| @Deprecated(since = "4.0.0") |
| protected boolean multiReleaseOutput; |
| |
| /** |
| * The file where to dump the command-line when debug is activated or when the compilation failed. |
| * For example, if the value is {@code "javac"}, then the Java compiler can be launched from the |
| * command-line by typing {@code javac @target/javac.args}. |
| * The debug file will contain the compiler options together with the list of source files to compile. |
| * |
| * <p>By default, this debug file is written only if the compilation of main code failed. |
| * The writing of the debug files can be forced by setting the {@link #verbose} flag to {@code true} |
| * or by specifying the {@code --verbose} option to Maven on the command-line.</p> |
| * |
| * @since 3.10.0 |
| */ |
| @Parameter(defaultValue = "javac.args") |
| protected String debugFileName; |
| |
| /** |
| * Target directory that have been temporarily created as symbolic link before compilation. |
| * This is used as a workaround for the fact that, when compiling a modular project with |
| * all the module-related compiler options, the classes are written in a directory with |
| * the module name. It does not fit in the {@code META-INF/versions/<release>} pattern. |
| * Temporary symbolic link is a workaround for this problem. |
| * |
| * <h4>Example</h4> |
| * When compiling the {@code my.app} module for Java 17, the desired output directory is: |
| * |
| * <blockquote>{@code target/classes/META-INF/versions/17}</blockquote> |
| * |
| * But {@code javac}, when used with the {@code --module-source-path} option, |
| * will write the classes in the following directory: |
| * |
| * <blockquote>{@code target/classes/META-INF/versions/17/my.app}</blockquote> |
| * |
| * We workaround this problem with a symbolic link which redirects {@code 17/my.app} to {@code 17}. |
| * We need to do this only when compiling multi-release project in the old deprecated way. |
| * When using the recommended {@code <sources>} approach, the plugins are designed to work |
| * with the directory layout produced by {@code javac} instead of fighting against it. |
| * |
| * @deprecated For compatibility with the previous way to build multi-release JAR file. |
| * May be removed after we drop support of the old way to do multi-release. |
| */ |
| @Deprecated(since = "4.0.0") |
| private ModuleDirectoryRemover directoryLevelToRemove; |
| |
| /** |
| * Creates a new compiler <abbr>MOJO</abbr> for the main code. |
| */ |
| public CompilerMojo() { |
| super(PathScope.MAIN_COMPILE); |
| } |
| |
| /** |
| * Runs the Java compiler on the main source code. |
| * If {@link #skipMain} is {@code true}, then this method logs a message and does nothing else. |
| * Otherwise, this method executes the steps described in the method of the parent class. |
| * |
| * @throws MojoException if the compiler cannot be run. |
| */ |
| @Override |
| public void execute() throws MojoException { |
| if (skipMain) { |
| logger.info("Not compiling main sources"); |
| return; |
| } |
| try { |
| super.execute(); |
| } finally { |
| try (ModuleDirectoryRemover r = directoryLevelToRemove) { |
| // Implicit call to directoryLevelToRemove.close(). |
| } catch (IOException e) { |
| throw new CompilationFailureException("I/O error while organizing multi-release classes.", e); |
| } |
| } |
| @SuppressWarnings("LocalVariableHidesMemberVariable") |
| Path outputDirectory = getOutputDirectory(); |
| if (Files.isDirectory(outputDirectory) && projectArtifact != null) { |
| artifactManager.setPath(projectArtifact, outputDirectory); |
| } |
| } |
| |
| /** |
| * Parses the parameters declared in the <abbr>MOJO</abbr>. |
| * |
| * @param compiler the tools to use for verifying the validity of options |
| * @return the options after validation |
| */ |
| @Override |
| @SuppressWarnings("deprecation") |
| public Options parseParameters(final OptionChecker compiler) { |
| Options configuration = super.parseParameters(compiler); |
| configuration.addUnchecked(compilerArgs); |
| configuration.addUnchecked(compilerArgument); |
| return configuration; |
| } |
| |
| /** |
| * {@return the path where to place generated source files created by annotation processing on the main classes} |
| */ |
| @Nullable |
| @Override |
| protected Path getGeneratedSourcesDirectory() { |
| return generatedSourcesDirectory; |
| } |
| |
| /** |
| * {@return the inclusion filters for the compiler, or an empty set for all Java source files} |
| */ |
| @Override |
| protected Set<String> getIncludes() { |
| return (includes != null) ? includes : Set.of(); |
| } |
| |
| /** |
| * {@return the exclusion filters for the compiler, or an empty set if none} |
| */ |
| @Override |
| protected Set<String> getExcludes() { |
| return (excludes != null) ? excludes : Set.of(); |
| } |
| |
| /** |
| * {@return the exclusion filters for the incremental calculation, or an empty set if none} |
| */ |
| @Override |
| protected Set<String> getIncrementalExcludes() { |
| return (incrementalExcludes != null) ? incrementalExcludes : Set.of(); |
| } |
| |
| /** |
| * {@return the destination directory for main class files} |
| * If {@link #multiReleaseOutput} is true <em>(deprecated)</em>, |
| * the output will be in a {@code META-INF/versions} subdirectory. |
| */ |
| @Nonnull |
| @Override |
| protected Path getOutputDirectory() { |
| if (SUPPORT_LEGACY && multiReleaseOutput && release != null) { |
| return DirectoryHierarchy.PACKAGE |
| .outputDirectoryForReleases(outputDirectory) |
| .resolve(release); |
| } |
| return outputDirectory; |
| } |
| |
| /** |
| * {@return the file where to dump the command-line when debug is activated or when the compilation failed} |
| * |
| * @see #debugFileName |
| */ |
| @Nullable |
| @Override |
| protected String getDebugFileName() { |
| return debugFileName; |
| } |
| |
| /** |
| * Creates a new task for compiling the main classes. |
| * |
| * @param listener where to send compilation warnings, or {@code null} for the Maven logger |
| * @throws MojoException if this method identifies an invalid parameter in this <abbr>MOJO</abbr> |
| * @return the task to execute for compiling the main code using the configuration in this <abbr>MOJO</abbr> |
| * @throws IOException if an error occurred while creating the output directory or scanning the source directories |
| */ |
| @Override |
| public ToolExecutor createExecutor(DiagnosticListener<? super JavaFileObject> listener) throws IOException { |
| ToolExecutor executor = super.createExecutor(listener); |
| if (SUPPORT_LEGACY && multiReleaseOutput) { |
| addImplicitDependencies(executor); |
| } |
| return executor; |
| } |
| |
| /** |
| * {@return whether the project has at least one module-info file} |
| * If no such file is found in the code to be compiled by this <abbr>MOJO</abbr> execution, |
| * then this method searches in the multi-release codes compiled by previous executions. |
| * |
| * @param roots root directories of the sources to compile |
| * @throws IOException if this method needed to read a module descriptor and failed |
| * |
| * @deprecated For compatibility with the previous way to build multi-release JAR file. |
| * May be removed after we drop support of the old way to do multi-release. |
| */ |
| @Override |
| @Deprecated(since = "4.0.0") |
| final boolean hasModuleDeclaration(final List<SourceDirectory> roots) throws IOException { |
| boolean hasModuleDeclaration = super.hasModuleDeclaration(roots); |
| if (SUPPORT_LEGACY && !hasModuleDeclaration && multiReleaseOutput) { |
| String type = project.getPackaging().type().id(); |
| if (!Type.CLASSPATH_JAR.equals(type)) { |
| for (Path p : getOutputDirectoryPerVersion().values()) { |
| p = p.resolve(SourceDirectory.MODULE_INFO + SourceDirectory.CLASS_FILE_SUFFIX); |
| if (Files.exists(p)) { |
| return true; |
| } |
| } |
| } |
| } |
| return hasModuleDeclaration; |
| } |
| |
| /** |
| * {@return the output directory of each target Java version} |
| * By convention, {@link SourceVersion#RELEASE_0} stands for the base version. |
| * |
| * @throws IOException if this method needs to walk through directories and that operation failed |
| * |
| * @deprecated For compatibility with the previous way to build multi-release JAR file. |
| * May be removed after we drop support of the old way to do multi-release. |
| */ |
| @Deprecated(since = "4.0.0") |
| private TreeMap<SourceVersion, Path> getOutputDirectoryPerVersion() throws IOException { |
| final Path root = DirectoryHierarchy.PACKAGE.outputDirectoryForReleases(outputDirectory); |
| if (Files.notExists(root)) { |
| return null; |
| } |
| final var paths = new TreeMap<SourceVersion, Path>(); |
| Files.walk(root, 1).forEach((path) -> { |
| SourceVersion version; |
| if (path.equals(root)) { |
| path = outputDirectory; |
| version = SourceVersion.RELEASE_0; |
| } else { |
| try { |
| version = SourceVersion.valueOf("RELEASE_" + path.getFileName()); |
| } catch (IllegalArgumentException e) { |
| throw new CompilationFailureException("Invalid version number for " + path, e); |
| } |
| } |
| if (paths.put(version, path) != null) { |
| throw new CompilationFailureException("Duplicated version number for " + path); |
| } |
| }); |
| return paths; |
| } |
| |
| /** |
| * Adds the compilation outputs of previous Java releases to the class-path of module-path. |
| * This method should be invoked only when compiling a multi-release <abbr>JAR</abbr> in the |
| * old deprecated way. |
| * |
| * <p>The {@code executor} argument may be {@code null} if the caller is only interested in the |
| * module name, with no executor to modify. The module name found by this method is specific to |
| * the way that projects are organized when {@link #multiReleaseOutput} is {@code true}.</p> |
| * |
| * @param executor the executor where to add implicit dependencies, or {@code null} if none |
| * @return the module name, or {@code null} if none |
| * @throws IOException if this method needs to walk through directories and that operation failed |
| * |
| * @deprecated For compatibility with the previous way to build multi-release JAR file. |
| * May be removed after we drop support of the old way to do multi-release. |
| */ |
| @Deprecated(since = "4.0.0") |
| private String addImplicitDependencies(final ToolExecutor executor) throws IOException { |
| final TreeMap<SourceVersion, Path> paths = getOutputDirectoryPerVersion(); |
| /* |
| * Search for the module name. If many module-info classes are found, |
| * the most basic one (with lowest Java release number) is selected. |
| */ |
| String moduleName = null; |
| for (Path path : paths.values()) { |
| path = path.resolve(MODULE_INFO + CLASS_FILE_SUFFIX); |
| if (Files.exists(path)) { |
| try (InputStream in = Files.newInputStream(path)) { |
| moduleName = ModuleDescriptor.read(in).name(); |
| } |
| break; |
| } |
| } |
| /* |
| * If no module name was found in the classes compiled for previous Java releases, |
| * search in the source files for the Java release of the current compilation unit. |
| */ |
| if (moduleName == null) { |
| final Stream<Path> sourceDirectories; |
| if (executor != null) { |
| sourceDirectories = executor.sourceDirectories.stream().map(dir -> dir.root); |
| } else if (isAbsent(compileSourceRoots)) { |
| sourceDirectories = getSourceRoots(compileScope.projectScope()).map(SourceRoot::directory); |
| } else { |
| sourceDirectories = compileSourceRoots.stream().map(Path::of); |
| } |
| for (Path root : sourceDirectories.toList()) { |
| moduleName = parseModuleInfoName(root.resolve(MODULE_INFO + JAVA_FILE_SUFFIX)); |
| if (moduleName != null) { |
| break; |
| } |
| } |
| } |
| if (executor != null) { |
| /* |
| * Add previous versions as dependencies on the class-path or module-path, depending on whether |
| * the project is modular. Each path should be on either the class-path or module-path, but not |
| * both. If a path for a modular project seems needed on the class-path, it may be a sign that |
| * other options are not used correctly (e.g., `--source-path` versus `--module-source-path`). |
| */ |
| PathType type = JavaPathType.CLASSES; |
| if (moduleName != null) { |
| type = JavaPathType.patchModule(moduleName); |
| directoryLevelToRemove = ModuleDirectoryRemover.create(executor.outputDirectory, moduleName); |
| } |
| if (!paths.isEmpty()) { |
| executor.dependencies(type).addAll(paths.descendingMap().values()); |
| } |
| } |
| return moduleName; |
| } |
| |
| /** |
| * {@return the module name in a previous execution of the compiler plugin, or {@code null} if none} |
| * |
| * @deprecated For compatibility with the previous way to build multi-release JAR file. |
| * May be removed after we drop support of the old way to do multi-release. |
| */ |
| @Override |
| @Deprecated(since = "4.0.0") |
| final String moduleOfPreviousExecution() throws IOException { |
| if (SUPPORT_LEGACY && multiReleaseOutput) { |
| return addImplicitDependencies(null); |
| } |
| return super.moduleOfPreviousExecution(); |
| } |
| } |