blob: 52da7ca8d136ae05b159c02f28d65c8bbd85c597 [file] [log] [blame]
/*
* 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 java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.utils.StringUtils;
import org.apache.maven.shared.utils.logging.MessageUtils;
import org.apache.maven.toolchain.Toolchain;
import org.apache.maven.toolchain.java.DefaultJavaToolChain;
import org.codehaus.plexus.compiler.util.scan.SimpleSourceInclusionScanner;
import org.codehaus.plexus.compiler.util.scan.SourceInclusionScanner;
import org.codehaus.plexus.compiler.util.scan.StaleSourceScanner;
import org.codehaus.plexus.languages.java.jpms.JavaModuleDescriptor;
import org.codehaus.plexus.languages.java.jpms.LocationManager;
import org.codehaus.plexus.languages.java.jpms.ModuleNameSource;
import org.codehaus.plexus.languages.java.jpms.ResolvePathsRequest;
import org.codehaus.plexus.languages.java.jpms.ResolvePathsResult;
/**
* Compiles application sources.
* By default uses the <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html">javac</a> compiler
* of the JDK used to execute Maven. This can be overwritten through <a href="https://maven.apache.org/guides/mini/guide-using-toolchains.html">Toolchains</a>
* or parameter {@link AbstractCompilerMojo#compilerId}.
*
* @author <a href="mailto:jason@maven.org">Jason van Zyl </a>
* @since 2.0
* @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html">javac Command</a>
*/
@Mojo(
name = "compile",
defaultPhase = LifecyclePhase.COMPILE,
threadSafe = true,
requiresDependencyResolution = ResolutionScope.COMPILE)
public class CompilerMojo extends AbstractCompilerMojo {
/**
* The source directories containing the sources to be compiled.
*/
@Parameter(defaultValue = "${project.compileSourceRoots}", readonly = false, required = true)
private List<String> compileSourceRoots;
/**
* The directory for compiled classes.
* <p>
* This parameter should only be modified in special cases. One example is creating
* a multi-release jar with a lower bytecode level (i.e. setting it to
* {@code ${project.build.outputDirectory}/META-INF/versions/21} or similar) in an additional
* execution.
* <p>
* When the required bytecode level is available though an installed JDK or toolchain,
* it is recommended to use the {@code <release>} property
* in conjunction with the ${multiReleaseOutput} parameter instead.
*/
@Parameter(
property = "maven.compiler.outputDirectory",
defaultValue = "${project.build.outputDirectory}",
required = true,
readonly = false)
private File outputDirectory;
/**
* Projects main artifact.
*
* @todo this is an export variable, really
*/
@Parameter(defaultValue = "${project.artifact}", readonly = true, required = true)
private Artifact projectArtifact;
/**
* A list of inclusion filters for the compiler.
*/
@Parameter
private Set<String> includes = new HashSet<>();
/**
* A list of exclusion filters for the compiler.
*/
@Parameter
private Set<String> excludes = new HashSet<>();
/**
* A list of exclusion filters for the incremental calculation.
* @since 3.11
*/
@Parameter
private Set<String> incrementalExcludes = new HashSet<>();
/**
* Specify where to place generated source files created by annotation processing. Only applies to JDK 1.6+
*
* @since 2.2
*/
@Parameter(defaultValue = "${project.build.directory}/generated-sources/annotations")
private File generatedSourcesDirectory;
/**
* 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")
private boolean skipMain;
@Parameter(defaultValue = "${project.compileClasspathElements}", readonly = true, required = true)
private List<String> compilePath;
/**
* <p>
* When set to {@code true}, the classes will be placed in <code>META-INF/versions/${release}</code> The release
* value must be set, otherwise the plugin will fail.
* </p>
* <strong>Note: </strong> A jar is only a multirelease jar if <code>META-INF/MANIFEST.MF</code> contains
* <code>Multi-Release: true</code>. 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 multirelease jar using the outputDirectory.
*
* @since 3.7.1
*/
@Parameter
private boolean multiReleaseOutput;
/**
* When both {@link AbstractCompilerMojo#fork} and {@link AbstractCompilerMojo#debug} are enabled the commandline arguments used
* will be dumped to this file.
* @since 3.10.0
*/
@Parameter(defaultValue = "javac")
private String debugFileName;
final LocationManager locationManager = new LocationManager();
private List<String> classpathElements;
private List<String> modulepathElements;
private Map<String, JavaModuleDescriptor> pathElements;
@Override
protected List<String> getCompileSourceRoots() {
return compileSourceRoots;
}
@Override
protected List<String> getClasspathElements() {
return classpathElements;
}
@Override
protected List<String> getModulepathElements() {
return modulepathElements;
}
@Override
protected Map<String, JavaModuleDescriptor> getPathElements() {
return pathElements;
}
@Override
protected File getOutputDirectory() {
File dir;
if (!multiReleaseOutput) {
dir = outputDirectory;
} else {
dir = new File(outputDirectory, "META-INF/versions/" + release);
}
return dir;
}
@Override
public void execute() throws MojoExecutionException, CompilationFailureException {
if (skipMain) {
getLog().info("Not compiling main sources");
return;
}
if (multiReleaseOutput && release == null) {
throw new MojoExecutionException("When using 'multiReleaseOutput' the release must be set");
}
super.execute();
if (outputDirectory.isDirectory()) {
File artifactFile = projectArtifact.getFile();
if (artifactFile != null && !Objects.equals(artifactFile, outputDirectory)) {
getLog().warn("Overwriting artifact's file from " + artifactFile + " to " + outputDirectory);
}
projectArtifact.setFile(outputDirectory);
}
}
@Override
protected Set<String> getIncludes() {
return includes;
}
@Override
protected Set<String> getExcludes() {
return excludes;
}
@Override
protected void preparePaths(Set<File> sourceFiles) {
// assert compilePath != null;
Optional<Path> moduleDeclaration = getModuleDeclaration(sourceFiles);
if (moduleDeclaration.isPresent()) {
// For now only allow named modules. Once we can create a graph with ASM we can specify exactly the modules
// and we can detect if auto modules are used. In that case, MavenProject.setFile() should not be used, so
// you cannot depend on this project and so it won't be distributed.
modulepathElements = new ArrayList<>(compilePath.size());
classpathElements = new ArrayList<>(compilePath.size());
pathElements = new LinkedHashMap<>(compilePath.size());
ResolvePathsResult<File> resolvePathsResult;
try {
Collection<File> dependencyArtifacts = getCompileClasspathElements(getProject());
ResolvePathsRequest<File> request = ResolvePathsRequest.ofFiles(dependencyArtifacts)
.setIncludeStatic(true)
.setMainModuleDescriptor(moduleDeclaration.get().toFile());
Toolchain toolchain = getToolchain();
if (toolchain instanceof DefaultJavaToolChain) {
request.setJdkHome(new File(((DefaultJavaToolChain) toolchain).getJavaHome()));
}
resolvePathsResult = locationManager.resolvePaths(request);
for (Entry<File, Exception> pathException :
resolvePathsResult.getPathExceptions().entrySet()) {
Throwable cause = pathException.getValue();
while (cause.getCause() != null) {
cause = cause.getCause();
}
String fileName = pathException.getKey().getName();
getLog().warn("Can't extract module name from " + fileName + ": " + cause.getMessage());
}
JavaModuleDescriptor moduleDescriptor = resolvePathsResult.getMainModuleDescriptor();
detectFilenameBasedAutomodules(resolvePathsResult, moduleDescriptor);
for (Map.Entry<File, JavaModuleDescriptor> entry :
resolvePathsResult.getPathElements().entrySet()) {
pathElements.put(entry.getKey().getPath(), entry.getValue());
}
if (compilerArgs == null) {
compilerArgs = new ArrayList<>();
}
for (File file : resolvePathsResult.getClasspathElements()) {
classpathElements.add(file.getPath());
if (multiReleaseOutput) {
if (getOutputDirectory().toPath().startsWith(file.getPath())) {
compilerArgs.add("--patch-module");
compilerArgs.add(String.format("%s=%s", moduleDescriptor.name(), file.getPath()));
}
}
}
for (File file : resolvePathsResult.getModulepathElements().keySet()) {
modulepathElements.add(file.getPath());
}
compilerArgs.add("--module-version");
compilerArgs.add(getProject().getVersion());
} catch (IOException e) {
getLog().warn(e.getMessage());
}
} else {
classpathElements = new ArrayList<>();
for (File element : getCompileClasspathElements(getProject())) {
classpathElements.add(element.getPath());
}
modulepathElements = Collections.emptyList();
pathElements = Collections.emptyMap();
}
}
private void detectFilenameBasedAutomodules(
final ResolvePathsResult<File> resolvePathsResult, final JavaModuleDescriptor moduleDescriptor) {
List<String> automodulesDetected = new ArrayList<>();
for (Entry<File, ModuleNameSource> entry :
resolvePathsResult.getModulepathElements().entrySet()) {
if (ModuleNameSource.FILENAME.equals(entry.getValue())) {
automodulesDetected.add(entry.getKey().getName());
}
}
if (!automodulesDetected.isEmpty()) {
final String message = "Required filename-based automodules detected: "
+ automodulesDetected + ". "
+ "Please don't publish this project to a public artifact repository!";
if (moduleDescriptor.exports().isEmpty()) {
// application
getLog().info(message);
} else {
// library
writeBoxedWarning(message);
}
}
}
private List<File> getCompileClasspathElements(MavenProject project) {
// 3 is outputFolder + 2 preserved for multirelease
List<File> list = new ArrayList<>(project.getArtifacts().size() + 3);
if (multiReleaseOutput) {
File versionsFolder = new File(project.getBuild().getOutputDirectory(), "META-INF/versions");
// in reverse order
for (int version = Integer.parseInt(getRelease()) - 1; version >= 9; version--) {
File versionSubFolder = new File(versionsFolder, String.valueOf(version));
if (versionSubFolder.exists()) {
list.add(versionSubFolder);
}
}
}
list.add(new File(project.getBuild().getOutputDirectory()));
for (Artifact a : project.getArtifacts()) {
if (a.getArtifactHandler().isAddedToClasspath()) {
list.add(a.getFile());
}
}
return list;
}
@Override
protected SourceInclusionScanner getSourceInclusionScanner(int staleMillis) {
if (includes.isEmpty() && excludes.isEmpty() && incrementalExcludes.isEmpty()) {
return new StaleSourceScanner(staleMillis);
}
if (includes.isEmpty()) {
includes.add("**/*.java");
}
Set<String> excludesIncr = new HashSet<>(excludes);
excludesIncr.addAll(this.incrementalExcludes);
return new StaleSourceScanner(staleMillis, includes, excludesIncr);
}
@Override
protected SourceInclusionScanner getSourceInclusionScanner(String inputFileEnding) {
// it's not defined if we get the ending with or without the dot '.'
String defaultIncludePattern = "**/*" + (inputFileEnding.startsWith(".") ? "" : ".") + inputFileEnding;
if (includes.isEmpty()) {
includes.add(defaultIncludePattern);
}
Set<String> excludesIncr = new HashSet<>(excludes);
excludesIncr.addAll(excludesIncr);
return new SimpleSourceInclusionScanner(includes, excludesIncr);
}
@Override
protected String getSource() {
return source;
}
@Override
protected String getTarget() {
return target;
}
@Override
protected String getRelease() {
return release;
}
@Override
protected String getCompilerArgument() {
return compilerArgument;
}
@Override
protected Map<String, String> getCompilerArguments() {
return compilerArguments;
}
@Override
protected File getGeneratedSourcesDirectory() {
return generatedSourcesDirectory;
}
@Override
protected String getDebugFileName() {
return debugFileName;
}
private void writeBoxedWarning(String message) {
String line = StringUtils.repeat("*", message.length() + 4);
getLog().warn(line);
getLog().warn("* " + MessageUtils.buffer().strong(message) + " *");
getLog().warn(line);
}
}