View Javadoc
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 java.io.File;
22  import java.io.IOException;
23  import java.nio.file.Files;
24  import java.nio.file.Path;
25  import java.nio.file.Paths;
26  import java.util.ArrayList;
27  import java.util.Collection;
28  import java.util.Collections;
29  import java.util.HashSet;
30  import java.util.LinkedHashMap;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.Map.Entry;
34  import java.util.Optional;
35  import java.util.Set;
36  import java.util.stream.Collectors;
37  import java.util.stream.Stream;
38  
39  import org.apache.maven.api.*;
40  import org.apache.maven.api.plugin.MojoException;
41  import org.apache.maven.api.plugin.annotations.Mojo;
42  import org.apache.maven.api.plugin.annotations.Parameter;
43  import org.apache.maven.api.services.MessageBuilderFactory;
44  import org.codehaus.plexus.compiler.util.scan.SimpleSourceInclusionScanner;
45  import org.codehaus.plexus.compiler.util.scan.SourceInclusionScanner;
46  import org.codehaus.plexus.compiler.util.scan.StaleSourceScanner;
47  import org.codehaus.plexus.languages.java.jpms.JavaModuleDescriptor;
48  import org.codehaus.plexus.languages.java.jpms.LocationManager;
49  import org.codehaus.plexus.languages.java.jpms.ModuleNameSource;
50  import org.codehaus.plexus.languages.java.jpms.ResolvePathsRequest;
51  import org.codehaus.plexus.languages.java.jpms.ResolvePathsResult;
52  import org.codehaus.plexus.util.StringUtils;
53  
54  /**
55   * Compiles application sources.
56   * By default uses the <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html">javac</a> compiler
57   * 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>
58   * or parameter {@link AbstractCompilerMojo#compilerId}.
59   *
60   * @author <a href="mailto:jason@maven.org">Jason van Zyl </a>
61   * @since 2.0
62   * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html">javac Command</a>
63   */
64  @Mojo(name = "compile", defaultPhase = "compile")
65  public class CompilerMojo extends AbstractCompilerMojo {
66      /**
67       * The source directories containing the sources to be compiled.
68       */
69      @Parameter
70      protected List<String> compileSourceRoots;
71  
72      /**
73       * Projects main artifact.
74       */
75      @Parameter(defaultValue = "${project.mainArtifact}", readonly = true, required = true)
76      protected Artifact projectArtifact;
77  
78      /**
79       * The directory for compiled classes.
80       */
81      @Parameter(defaultValue = "${project.build.outputDirectory}", required = true, readonly = true)
82      protected Path outputDirectory;
83  
84      /**
85       * A list of inclusion filters for the compiler.
86       */
87      @Parameter
88      protected Set<String> includes = new HashSet<>();
89  
90      /**
91       * A list of exclusion filters for the compiler.
92       */
93      @Parameter
94      protected Set<String> excludes = new HashSet<>();
95  
96      /**
97       * A list of exclusion filters for the incremental calculation.
98       * @since 3.11
99       */
100     @Parameter
101     protected Set<String> incrementalExcludes = new HashSet<>();
102 
103     /**
104      * <p>
105      * Specify where to place generated source files created by annotation processing. Only applies to JDK 1.6+
106      * </p>
107      *
108      * @since 2.2
109      */
110     @Parameter(defaultValue = "${project.build.directory}/generated-sources/annotations")
111     protected Path generatedSourcesDirectory;
112 
113     /**
114      * Set this to {@code true} to bypass compilation of main sources. Its use is NOT RECOMMENDED, but quite convenient on
115      * occasion.
116      */
117     @Parameter(property = "maven.main.skip")
118     protected boolean skipMain;
119 
120     @Parameter
121     protected List<String> compilePath;
122 
123     /**
124      * <p>
125      * When set to {@code true}, the classes will be placed in <code>META-INF/versions/${release}</code> The release
126      * value must be set, otherwise the plugin will fail.
127      * </p>
128      * <strong>Note: </strong> A jar is only a multirelease jar if <code>META-INF/MANIFEST.MF</code> contains
129      * <code>Multi-Release: true</code>. You need to set this by configuring the <a href=
130      * "https://maven.apache.org/plugins/maven-jar-plugin/examples/manifest-customization.html">maven-jar-plugin</a>.
131      * This implies that you cannot test a multirelease jar using the outputDirectory.
132      *
133      * @since 3.7.1
134      */
135     @Parameter
136     protected boolean multiReleaseOutput;
137 
138     /**
139      * When both {@link AbstractCompilerMojo#fork} and {@link AbstractCompilerMojo#debug} are enabled the commandline arguments used
140      * will be dumped to this file.
141      * @since 3.10.0
142      */
143     @Parameter(defaultValue = "javac")
144     protected String debugFileName;
145 
146     final LocationManager locationManager = new LocationManager();
147 
148     private List<String> classpathElements;
149 
150     private List<String> modulepathElements;
151 
152     private Map<String, JavaModuleDescriptor> pathElements;
153 
154     protected List<Path> getCompileSourceRoots() {
155         if (compileSourceRoots == null || compileSourceRoots.isEmpty()) {
156             return projectManager.getCompileSourceRoots(getProject(), ProjectScope.MAIN);
157         } else {
158             return compileSourceRoots.stream().map(Paths::get).collect(Collectors.toList());
159         }
160     }
161 
162     @Override
163     protected List<String> getClasspathElements() {
164         return classpathElements;
165     }
166 
167     @Override
168     protected List<String> getModulepathElements() {
169         return modulepathElements;
170     }
171 
172     @Override
173     protected Map<String, JavaModuleDescriptor> getPathElements() {
174         return pathElements;
175     }
176 
177     protected Path getOutputDirectory() {
178         Path dir;
179         if (!multiReleaseOutput) {
180             dir = outputDirectory;
181         } else {
182             dir = outputDirectory.resolve("META-INF/versions/" + release);
183         }
184         return dir;
185     }
186 
187     public void execute() throws MojoException {
188         if (skipMain) {
189             getLog().info("Not compiling main sources");
190             return;
191         }
192 
193         if (multiReleaseOutput && release == null) {
194             throw new MojoException("When using 'multiReleaseOutput' the release must be set");
195         }
196 
197         super.execute();
198 
199         if (Files.isDirectory(outputDirectory) && projectArtifact != null) {
200             artifactManager.setPath(projectArtifact, outputDirectory);
201         }
202     }
203 
204     @Override
205     protected Set<String> getIncludes() {
206         return includes;
207     }
208 
209     @Override
210     protected Set<String> getExcludes() {
211         return excludes;
212     }
213 
214     @Override
215     protected void preparePaths(Set<Path> sourceFiles) {
216         // assert compilePath != null;
217         List<String> compilePath = this.compilePath;
218         if (compilePath == null) {
219             Stream<String> s1 = Stream.of(getOutputDirectory().toString());
220             Stream<String> s2 = session.resolveDependencies(getProject(), PathScope.MAIN_COMPILE).stream()
221                     .map(Path::toString);
222             compilePath = Stream.concat(s1, s2).collect(Collectors.toList());
223         }
224 
225         Path moduleDescriptorPath = null;
226 
227         boolean hasModuleDescriptor = false;
228         for (Path sourceFile : sourceFiles) {
229             if ("module-info.java".equals(sourceFile.getFileName().toString())) {
230                 moduleDescriptorPath = sourceFile;
231                 hasModuleDescriptor = true;
232                 break;
233             }
234         }
235 
236         if (hasModuleDescriptor) {
237             // For now only allow named modules. Once we can create a graph with ASM we can specify exactly the modules
238             // and we can detect if auto modules are used. In that case, MavenProject.setFile() should not be used, so
239             // you cannot depend on this project and so it won't be distributed.
240 
241             modulepathElements = new ArrayList<>(compilePath.size());
242             classpathElements = new ArrayList<>(compilePath.size());
243             pathElements = new LinkedHashMap<>(compilePath.size());
244 
245             ResolvePathsResult<File> resolvePathsResult;
246             try {
247                 Collection<File> dependencyArtifacts = getCompileClasspathElements(getProject()).stream()
248                         .map(Path::toFile)
249                         .collect(Collectors.toList());
250 
251                 ResolvePathsRequest<File> request = ResolvePathsRequest.ofFiles(dependencyArtifacts)
252                         .setIncludeStatic(true)
253                         .setMainModuleDescriptor(moduleDescriptorPath.toFile());
254 
255                 Optional<Toolchain> toolchain = getToolchain();
256                 if (toolchain.isPresent() && toolchain.get() instanceof JavaToolchain) {
257                     request.setJdkHome(new File(((JavaToolchain) toolchain.get()).getJavaHome()));
258                 }
259 
260                 resolvePathsResult = locationManager.resolvePaths(request);
261 
262                 for (Entry<File, Exception> pathException :
263                         resolvePathsResult.getPathExceptions().entrySet()) {
264                     Throwable cause = pathException.getValue();
265                     while (cause.getCause() != null) {
266                         cause = cause.getCause();
267                     }
268                     String fileName = pathException.getKey().getName();
269                     getLog().warn("Can't extract module name from " + fileName + ": " + cause.getMessage());
270                 }
271 
272                 JavaModuleDescriptor moduleDescriptor = resolvePathsResult.getMainModuleDescriptor();
273 
274                 detectFilenameBasedAutomodules(resolvePathsResult, moduleDescriptor);
275 
276                 for (Map.Entry<File, JavaModuleDescriptor> entry :
277                         resolvePathsResult.getPathElements().entrySet()) {
278                     pathElements.put(entry.getKey().getPath(), entry.getValue());
279                 }
280 
281                 if (compilerArgs == null) {
282                     compilerArgs = new ArrayList<>();
283                 }
284 
285                 for (File file : resolvePathsResult.getClasspathElements()) {
286                     classpathElements.add(file.getPath());
287 
288                     if (multiReleaseOutput) {
289                         if (getOutputDirectory().startsWith(file.getPath())) {
290                             compilerArgs.add("--patch-module");
291                             compilerArgs.add(String.format("%s=%s", moduleDescriptor.name(), file.getPath()));
292                         }
293                     }
294                 }
295 
296                 for (File file : resolvePathsResult.getModulepathElements().keySet()) {
297                     modulepathElements.add(file.getPath());
298                 }
299 
300                 compilerArgs.add("--module-version");
301                 compilerArgs.add(getProject().getVersion());
302 
303             } catch (IOException e) {
304                 getLog().warn(e.getMessage());
305             }
306         } else {
307             classpathElements = new ArrayList<>();
308             for (Path element : getCompileClasspathElements(getProject())) {
309                 classpathElements.add(element.toString());
310             }
311             modulepathElements = Collections.emptyList();
312         }
313     }
314 
315     private void detectFilenameBasedAutomodules(
316             final ResolvePathsResult<File> resolvePathsResult, final JavaModuleDescriptor moduleDescriptor) {
317         List<String> automodulesDetected = new ArrayList<>();
318         for (Entry<File, ModuleNameSource> entry :
319                 resolvePathsResult.getModulepathElements().entrySet()) {
320             if (ModuleNameSource.FILENAME.equals(entry.getValue())) {
321                 automodulesDetected.add(entry.getKey().getName());
322             }
323         }
324 
325         if (!automodulesDetected.isEmpty()) {
326             final String message = "Required filename-based automodules detected: "
327                     + automodulesDetected + ". "
328                     + "Please don't publish this project to a public artifact repository!";
329 
330             if (moduleDescriptor.exports().isEmpty()) {
331                 // application
332                 getLog().info(message);
333             } else {
334                 // library
335                 writeBoxedWarning(message);
336             }
337         }
338     }
339 
340     private List<Path> getCompileClasspathElements(Project project) {
341         List<Path> artifacts = session.resolveDependencies(project, PathScope.MAIN_COMPILE);
342 
343         // 3 is outputFolder + 2 preserved for multirelease
344         List<Path> list = new ArrayList<>(artifacts.size() + 3);
345 
346         if (multiReleaseOutput) {
347             Path versionsFolder = outputDirectory.resolve("META-INF/versions");
348 
349             // in reverse order
350             for (int version = Integer.parseInt(getRelease()) - 1; version >= 9; version--) {
351                 Path versionSubFolder = versionsFolder.resolve(String.valueOf(version));
352                 if (Files.exists(versionSubFolder)) {
353                     list.add(versionSubFolder);
354                 }
355             }
356         }
357 
358         list.add(outputDirectory);
359 
360         list.addAll(artifacts);
361 
362         return list;
363     }
364 
365     protected SourceInclusionScanner getSourceInclusionScanner(int staleMillis) {
366         if (includes.isEmpty() && excludes.isEmpty() && incrementalExcludes.isEmpty()) {
367             return new StaleSourceScanner(staleMillis);
368         }
369 
370         if (includes.isEmpty()) {
371             includes.add("**/*.java");
372         }
373 
374         Set<String> excludesIncr = new HashSet<>(excludes);
375         excludesIncr.addAll(this.incrementalExcludes);
376         return new StaleSourceScanner(staleMillis, includes, excludesIncr);
377     }
378 
379     protected SourceInclusionScanner getSourceInclusionScanner(String inputFileEnding) {
380         // it's not defined if we get the ending with or without the dot '.'
381         String defaultIncludePattern = "**/*" + (inputFileEnding.startsWith(".") ? "" : ".") + inputFileEnding;
382 
383         if (includes.isEmpty()) {
384             includes.add(defaultIncludePattern);
385         }
386         Set<String> excludesIncr = new HashSet<>(excludes);
387         excludesIncr.addAll(excludesIncr);
388         return new SimpleSourceInclusionScanner(includes, excludesIncr);
389     }
390 
391     @Override
392     protected String getSource() {
393         return source;
394     }
395 
396     @Override
397     protected String getTarget() {
398         return target;
399     }
400 
401     @Override
402     protected String getRelease() {
403         return release;
404     }
405 
406     @Override
407     protected String getCompilerArgument() {
408         return compilerArgument;
409     }
410 
411     protected Path getGeneratedSourcesDirectory() {
412         return generatedSourcesDirectory;
413     }
414 
415     @Override
416     protected String getDebugFileName() {
417         return debugFileName;
418     }
419 
420     private void writeBoxedWarning(String message) {
421         String line = StringUtils.repeat("*", message.length() + 4);
422         getLog().warn(line);
423         getLog().warn("* " + strong(message) + " *");
424         getLog().warn(line);
425     }
426 
427     private String strong(String message) {
428         return session.getService(MessageBuilderFactory.class)
429                 .builder()
430                 .strong(message)
431                 .build();
432     }
433 }