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.plugins.jmod;
20  
21  import javax.inject.Inject;
22  
23  import java.io.File;
24  import java.io.IOException;
25  import java.util.ArrayList;
26  import java.util.Collection;
27  import java.util.Collections;
28  import java.util.List;
29  import java.util.Map;
30  
31  import org.apache.maven.artifact.Artifact;
32  import org.apache.maven.plugin.MojoExecutionException;
33  import org.apache.maven.plugin.MojoFailureException;
34  import org.apache.maven.plugins.annotations.LifecyclePhase;
35  import org.apache.maven.plugins.annotations.Mojo;
36  import org.apache.maven.plugins.annotations.Parameter;
37  import org.apache.maven.plugins.annotations.ResolutionScope;
38  import org.apache.maven.project.MavenProject;
39  import org.apache.maven.shared.utils.StringUtils;
40  import org.apache.maven.shared.utils.cli.Commandline;
41  import org.apache.maven.shared.utils.io.FileUtils;
42  import org.apache.maven.shared.utils.logging.MessageUtils;
43  import org.apache.maven.toolchain.Toolchain;
44  import org.apache.maven.toolchain.ToolchainManager;
45  import org.apache.maven.toolchain.java.DefaultJavaToolChain;
46  import org.codehaus.plexus.languages.java.jpms.JavaModuleDescriptor;
47  import org.codehaus.plexus.languages.java.jpms.LocationManager;
48  import org.codehaus.plexus.languages.java.jpms.ModuleNameSource;
49  import org.codehaus.plexus.languages.java.jpms.ResolvePathsRequest;
50  import org.codehaus.plexus.languages.java.jpms.ResolvePathsResult;
51  
52  /**
53   * The <code>create</code> goal is intended to create <code>jmod</code> files which can be used for later linking via
54   * <a href="https://maven.apache.org/plugins/maven-jlink-plugin/">maven-jlink-plugin</a>. The <code>jmod</code> files
55   * can not be used as usual dependencies on the classpath only in relationship with <code>maven-jlink-plugin</code>.
56   *
57   * @author Karl Heinz Marbaise <a href="mailto:khmarbaise@apache.org">khmarbaise@apache.org</a>
58   */
59  // CHECKSTYLE_OFF: LineLength
60  @Mojo(
61          name = "create",
62          requiresDependencyResolution = ResolutionScope.RUNTIME,
63          defaultPhase = LifecyclePhase.PACKAGE,
64          requiresProject = true)
65  // CHECKSTYLE_ON: LineLength
66  public class JModCreateMojo extends AbstractJModMojo {
67      private static final String JMODS = "jmods";
68  
69      private List<String> classpathElements;
70  
71      private List<String> modulepathElements;
72  
73      @Parameter(defaultValue = "${project.compileClasspathElements}", readonly = true, required = true)
74      private List<String> compilePath;
75  
76      private final LocationManager locationManager;
77  
78      /**
79       * Specifies one or more directories containing native commands to be copied. The given directories are relative to
80       * the current base directory. If no entry is defined the default is <code>src/main/cmds</code> used.
81       *
82       * <pre>
83       * &lt;cmds&gt;
84       *   &lt;cmd&gt;...&lt;/cmd&gt;
85       *   &lt;cmd&gt;...&lt;/cmd&gt;
86       *   .
87       *   .
88       * &lt;/cmds&gt;
89       * </pre>
90       * <p>
91       * All files from those directories will be copied into the resulting directory <code>bin</code> within the jmod
92       * file.
93       * </p>
94       * <code>JMod</code> command line equivalent: <code>--cmds &lt;path&gt;</code>.
95       */
96      @Parameter
97      private List<String> cmds;
98  
99      private static final String DEFAULT_CMD_DIRECTORY = "src/main/cmds";
100 
101     /**
102      * Specifies one or more directories containing configuration files to be copied. Location of user-editable config
103      * files. If no configuration is given the <code>src/main/configs</code> location is used as default. If this
104      * directory does not exist the whole will be ignored.
105      *
106      * <pre>
107      * &lt;configs&gt;
108      *   &lt;config&gt;...&lt;/config&gt;
109      *   &lt;config&gt;...&lt;/config&gt;
110      *   .
111      *   .
112      * &lt;/configs&gt;
113      * </pre>
114      * <p>
115      * All files from those directories will be copied into the resulting directory <code>config</code> within the jmod
116      * file.
117      * </p>
118      * jmod command line equivalent: <code>--config &lt;path&gt;</code>.
119      */
120     @Parameter
121     private List<String> configs;
122 
123     private static final String DEFAULT_CONFIG_DIRECTORY = "src/main/configs";
124 
125     /**
126      * Exclude files matching the pattern list. Each element using one the following forms: &lt;glob-pattern&gt;,
127      * glob:&lt;glob-pattern&gt; or regex:&lt;regex-pattern&gt;
128      *
129      * <pre>
130      * &lt;excludes&gt;
131      *   &lt;exclude&gt;...&lt;/exclude&gt;
132      *   &lt;exclude&gt;...&lt;/exclude&gt;
133      *   .
134      *   .
135      * &lt;/excludes&gt;
136      * </pre>
137      */
138     @Parameter
139     private List<String> excludes;
140 
141     /**
142      * Define the main class which is recorded in the <code>module-info.class</code> file.
143      */
144     @Parameter
145     private String mainClass;
146 
147     /**
148      * Specifies one or more directories containing native libraries to be copied (The given directories are relative to
149      * project base directory). If no configuration is given in <<pom.xml>> file the location <code>src/main/libs</code>
150      * will be used. If the default location does not exist the whole configuration will be ignored.
151      *
152      * <pre>
153      * &lt;libs&gt;
154      *   &lt;lib&gt;...&lt;/lib&gt;
155      *   &lt;lib&gt;...&lt;/lib&gt;
156      *   .
157      *   .
158      * &lt;/libs&gt;
159      * </pre>
160      * <p>
161      * All files from those directories will be copied into the resulting directory <code>lib</code> within the jmod
162      * file.
163      * </p>
164      */
165     @Parameter
166     private List<String> libs;
167 
168     private static final String DEFAULT_LIB_DIRECTORY = "src/main/libs";
169 
170     /**
171      * Define the module version of the jmod file.
172      */
173     @Parameter(defaultValue = "${project.version}")
174     private String moduleVersion;
175 
176     /**
177      * <code>--do-not-resolve-by-default</code> Exclude from the default root set of modules
178      */
179     @Parameter(defaultValue = "false")
180     private boolean doNotResolveByDefault;
181 
182     /**
183      * Define the locations of header files. The default location is <code>src/main/headerfiles</code>. If the the
184      * default location does not exist in the current project it will be ignored. The given directories are relative to
185      * the project base directory. If an entry is defined the definition of all locations is needed.
186      *
187      * <pre>
188      * &lt;headerFiles&gt;
189      *   &lt;headerFile&gt;...&lt;/headerFile&gt;
190      *   &lt;headerFile&gt;...&lt;/headerFile&gt;
191      *   .
192      *   .
193      * &lt;/headerFiles&gt;
194      * </pre>
195      * <p>
196      * All files from those directories will be copied into the resulting directory <code>includes</code> within the
197      * jmod file.
198      * </p>
199      * jmod command line equivalent <code>--header-files &lt;path&gt;</code>
200      */
201     @Parameter
202     private List<String> headerFiles;
203 
204     private static final String DEFAULT_HEADER_FILES_DIRECTORY = "src/main/headerfiles";
205 
206     /**
207      * Define the locations of man pages. The default location is <code>src/main/manpages</code>. The given man pages
208      * locations are relative to the project base directory.
209      *
210      * <pre>
211      * &lt;manPages&gt;
212      *   &lt;manPage&gt;...&lt;/manPage&gt;
213      *   &lt;manPage&gt;...&lt;/manPage&gt;
214      *   .
215      *   .
216      * &lt;/manPages&gt;
217      * </pre>
218      * <p>
219      * All files from those directories will be copied into the resulting directory <code>man</code> within the jmod
220      * file.
221      * </p>
222      * jmod command line equivalent <code>--man-pages &lt;path&gt;</code>
223      */
224     @Parameter
225     private List<String> manPages;
226 
227     private static final String DEFAULT_MAN_PAGES_DIRECTORY = "src/main/manpages";
228 
229     /**
230      * This is only the name of the jmod file in the target directory.
231      */
232     @Parameter(defaultValue = "${project.artifactId}", required = true, readonly = true)
233     private String outputFileName;
234 
235     /**
236      * Define the location of legal notices. The default location is <code>src/main/legalnotices</code>. The given man
237      * pages locations are relative to the project base directory.
238      *
239      * <pre>
240      * &lt;legalNotices&gt;
241      *   &lt;legalNotice&gt;...&lt;/legalNotice&gt;
242      *   &lt;legalNotice&gt;...&lt;/legalNotice&gt;
243      *   .
244      *   .
245      * &lt;/legalNotices&gt;
246      * </pre>
247      * <p>
248      * All files from those directories will be copied into the resulting directory <code>legal</code> within the jmod
249      * file.
250      * </p>
251      * jmod command line equivalent <code>--legal-notices &lt;path&gt;</code>
252      */
253     @Parameter
254     private List<String> legalNotices;
255 
256     private static final String DEFAULT_LEGAL_NOTICES_DIRECTORY = "src/main/legalnotices";
257 
258     /**
259      * <code>--target-platform &lt;target-platform&gt;</code> Target platform TODO: Which values are valid?
260      */
261     @Parameter
262     private String targetPlatform;
263 
264     /**
265      * Hint for a tool to issue a warning if the module is resolved. The valid values are:
266      * <ul>
267      * <li>deprecated</li>
268      * <li>deprecated-for-removal</li>
269      * <li>incubating</li>
270      * </ul>
271      */
272     @Parameter
273     private String warnIfResolved;
274 
275     @Parameter(defaultValue = "${project.build.outputDirectory}", required = true, readonly = true)
276     private File targetClassesDirectory;
277 
278     @Parameter(defaultValue = "${project.build.directory}", required = true, readonly = true)
279     private File outputDirectory;
280 
281     // calculated based on jmod(.exe)/../..
282     private File javaHome;
283 
284     @Inject
285     public JModCreateMojo(ToolchainManager toolchainManager, LocationManager locationManager) {
286         super(toolchainManager);
287         this.locationManager = locationManager;
288     }
289 
290     public void execute() throws MojoExecutionException, MojoFailureException {
291         try {
292             String jModExecutable = getJModExecutable();
293             File jModExecuteableFile = new File(jModExecutable);
294             javaHome = jModExecuteableFile.getParentFile().getParentFile();
295             File jmodsFolderJDK = new File(javaHome, JMODS);
296             getLog().debug("Parent: " + javaHome.getAbsolutePath());
297             getLog().debug("jmodsFolder: " + jmodsFolderJDK.getAbsolutePath());
298 
299             preparePaths();
300 
301             failIfParametersAreNotInTheirValidValueRanges();
302 
303             getLog().debug("Toolchain in maven-jmod-plugin: jmod [ " + jModExecutable + " ]");
304 
305             // We need to put the resulting x.jmod files into jmods folder otherwise is
306             // seemed to be not working.
307             // Check why?
308             File modsFolder = new File(outputDirectory, "jmods");
309             File resultingJModFile = new File(modsFolder, outputFileName + ".jmod");
310 
311             deleteOutputIfAlreadyExists(resultingJModFile);
312 
313             // create the jmods folder...
314             modsFolder.mkdirs();
315 
316             Commandline cmd = createJModCreateCommandLine(resultingJModFile);
317             cmd.setExecutable(jModExecutable);
318 
319             executeCommand(cmd, outputDirectory);
320 
321             if (projectHasAlreadySetAnArtifact()) {
322                 throw new MojoExecutionException("You have to use a classifier "
323                         + "to attach supplemental artifacts to the project instead of replacing them.");
324             }
325 
326             getProject().getArtifact().setFile(resultingJModFile);
327         } catch (IOException e) {
328             throw new MojoFailureException("Unable to find jmod command: " + e.getMessage(), e);
329         }
330     }
331 
332     private void deleteOutputIfAlreadyExists(File resultingJModFile) throws MojoFailureException {
333         if (resultingJModFile.exists() && resultingJModFile.isFile()) {
334             try {
335                 getLog().debug("Deleting the existing " + resultingJModFile.getAbsolutePath() + " file.");
336                 FileUtils.forceDelete(resultingJModFile);
337             } catch (IOException e) {
338                 String message = "Failure during deleting of file " + resultingJModFile.getAbsolutePath();
339                 getLog().error(message);
340                 throw new MojoFailureException(message);
341             }
342         }
343     }
344 
345     private void failIfParametersAreNotInTheirValidValueRanges() throws MojoFailureException {
346         if (warnIfResolved != null) {
347             String x = warnIfResolved.toLowerCase().trim();
348             if (!"deprecated".equals(x) && "deprecated-for-removal".equals(x) && "incubating".equals(x)) {
349                 String message = "The parameter warnIfResolved does not contain a valid value. "
350                         + "Valid values are 'deprecated', 'deprecated-for-removal' or 'incubating'.";
351                 getLog().error(message);
352                 throw new MojoFailureException(message);
353             }
354         }
355 
356         throwExceptionIfNotExistOrNotADirectory(handleConfigurationListWithDefault(cmds, DEFAULT_CMD_DIRECTORY), "cmd");
357         throwExceptionIfNotExistOrNotADirectory(
358                 handleConfigurationListWithDefault(configs, DEFAULT_CONFIG_DIRECTORY), "config");
359         throwExceptionIfNotExistOrNotADirectory(handleConfigurationListWithDefault(libs, DEFAULT_LIB_DIRECTORY), "lib");
360         throwExceptionIfNotExistOrNotADirectory(
361                 handleConfigurationListWithDefault(headerFiles, DEFAULT_HEADER_FILES_DIRECTORY), "headerFile");
362         throwExceptionIfNotExistOrNotADirectory(
363                 handleConfigurationListWithDefault(legalNotices, DEFAULT_LEGAL_NOTICES_DIRECTORY), "legalNotice");
364         throwExceptionIfNotExistOrNotADirectory(
365                 handleConfigurationListWithDefault(manPages, DEFAULT_MAN_PAGES_DIRECTORY), "manPage");
366     }
367 
368     private void throwExceptionIfNotExistOrNotADirectory(List<String> configurations, String partialMessage)
369             throws MojoFailureException {
370         for (String configLocation : configurations) {
371             File dir = new File(configLocation);
372             if (!dir.exists() || !dir.isDirectory()) {
373                 String message = "The directory " + configLocation + " for " + partialMessage
374                         + " parameter does not exist " + "or is not a directory. ";
375                 getLog().error(message);
376                 throw new MojoFailureException(message);
377             }
378         }
379     }
380 
381     private List<File> getCompileClasspathElements(MavenProject project) {
382         List<File> list = new ArrayList<File>(project.getArtifacts().size() + 1);
383 
384         if (targetClassesDirectory.exists()) {
385             list.add(new File(project.getBuild().getOutputDirectory()));
386         }
387 
388         for (Artifact a : project.getArtifacts()) {
389             list.add(a.getFile());
390         }
391         return list;
392     }
393 
394     private void preparePaths() {
395         boolean hasModuleDescriptor = false;
396 
397         // Assuming that the module-info.java is already compiled by compiler plugin so only
398         // check if the module-info.class file exists.
399         File moduleInfo = new File(targetClassesDirectory, "module-info.class");
400 
401         if (moduleInfo.exists() && moduleInfo.isFile()) {
402             getLog().debug("We have found a module-info.class file.");
403             hasModuleDescriptor = true;
404         }
405 
406         Collection<File> dependencyArtifacts = getCompileClasspathElements(getProject());
407 
408         if (hasModuleDescriptor) {
409             // For now only allow named modules. Once we can create a graph with ASM we can specify exactly the modules,
410             // and we can detect if auto modules are used. In that case, MavenProject.setFile() should not be used, so
411             // you cannot depend on this project and so it won't be distributed.
412 
413             modulepathElements = new ArrayList<>();
414             classpathElements = new ArrayList<>();
415 
416             ResolvePathsResult<File> resolvePathsResult;
417             try {
418 
419                 ResolvePathsRequest<File> request =
420                         ResolvePathsRequest.ofFiles(dependencyArtifacts).setMainModuleDescriptor(moduleInfo);
421 
422                 Toolchain toolchain = getToolchain();
423                 if (toolchain != null && toolchain instanceof DefaultJavaToolChain) {
424                     request.setJdkHome(new File(((DefaultJavaToolChain) toolchain).getJavaHome()));
425                 }
426 
427                 resolvePathsResult = locationManager.resolvePaths(request);
428 
429                 JavaModuleDescriptor moduleDescriptor = resolvePathsResult.getMainModuleDescriptor();
430 
431                 for (Map.Entry<File, ModuleNameSource> entry :
432                         resolvePathsResult.getModulepathElements().entrySet()) {
433                     getLog().debug("File: " + entry.getKey().getAbsolutePath() + " "
434                             + entry.getValue().name());
435                     if (ModuleNameSource.FILENAME.equals(entry.getValue())) {
436                         final String message = "Required filename-based automodules detected. "
437                                 + "Please don't publish this project to a public artifact repository!";
438 
439                         if (moduleDescriptor.exports().isEmpty()) {
440                             // application
441                             getLog().info(message);
442                         } else {
443                             // library
444                             writeBoxedWarning(message);
445                         }
446                         break;
447                     }
448                 }
449 
450                 for (File file : resolvePathsResult.getClasspathElements()) {
451                     getLog().debug("classpathElements: File: " + file.getPath());
452                     classpathElements.add(file.getPath());
453                 }
454 
455                 for (File file : resolvePathsResult.getModulepathElements().keySet()) {
456                     getLog().debug("modulepathElements: File: " + file.getPath());
457                     if (file.isDirectory()) {
458                         modulepathElements.add(file.getPath());
459                     } else {
460                         modulepathElements.add(file.getParent());
461                     }
462                 }
463             } catch (IOException e) {
464                 getLog().warn(e.getMessage());
465             }
466         } else {
467             modulepathElements = Collections.emptyList();
468 
469             classpathElements = new ArrayList<String>();
470             for (File file : dependencyArtifacts) {
471                 classpathElements.add(file.getPath());
472             }
473         }
474     }
475 
476     private Commandline createJModCreateCommandLine(File resultingJModFile) {
477         Commandline command = new Commandline();
478         command.createArg().setValue("create");
479         if (moduleVersion != null) {
480             command.createArg().setValue("--module-version=" + moduleVersion);
481         }
482 
483         List<String> classPaths;
484         if (classpathElements != null) {
485             classPaths = new ArrayList<>(classpathElements);
486         } else {
487             classPaths = new ArrayList<>(1);
488         }
489         if (targetClassesDirectory.exists()) {
490             classPaths.add(targetClassesDirectory.getAbsolutePath());
491         }
492 
493         command.createArg()
494                 .setValue("--class-path=" + getPlatformSeparatedList(classPaths).replace("\\", "\\\\"));
495 
496         if (excludes != null && !excludes.isEmpty()) {
497             String commaSeparatedList = getCommaSeparatedList(excludes);
498             command.createArg().setValue("--exclude=" + commaSeparatedList.replace("\\", "\\\\"));
499         }
500 
501         List<String> configList = handleConfigurationListWithDefault(configs, DEFAULT_CONFIG_DIRECTORY);
502         if (!configList.isEmpty()) {
503             command.createArg().setValue("--config=" + getPlatformSeparatedList(configList));
504         }
505 
506         if (StringUtils.isNotBlank(mainClass)) {
507             command.createArg().setValue("--main-class=" + mainClass);
508         }
509 
510         List<String> cmdsList = handleConfigurationListWithDefault(cmds, DEFAULT_CMD_DIRECTORY);
511         if (!cmdsList.isEmpty()) {
512             command.createArg().setValue("--cmds=" + getPlatformSeparatedList(cmdsList));
513         }
514 
515         List<String> libsList = handleConfigurationListWithDefault(libs, DEFAULT_LIB_DIRECTORY);
516         if (!libsList.isEmpty()) {
517             command.createArg().setValue("--libs=" + getPlatformSeparatedList(libsList));
518         }
519 
520         List<String> headerFilesList = handleConfigurationListWithDefault(headerFiles, DEFAULT_HEADER_FILES_DIRECTORY);
521         if (!headerFilesList.isEmpty()) {
522             command.createArg().setValue("--header-files=" + getPlatformSeparatedList(headerFilesList));
523         }
524 
525         List<String> legalNoticesList =
526                 handleConfigurationListWithDefault(legalNotices, DEFAULT_LEGAL_NOTICES_DIRECTORY);
527         if (!legalNoticesList.isEmpty()) {
528             command.createArg().setValue("--legal-notices=" + getPlatformSeparatedList(legalNoticesList));
529         }
530 
531         List<String> manPagesList = handleConfigurationListWithDefault(manPages, DEFAULT_MAN_PAGES_DIRECTORY);
532         if (!manPagesList.isEmpty()) {
533             command.createArg().setValue("--man-pages=" + getPlatformSeparatedList(manPagesList));
534         }
535 
536         List<String> modulePaths = new ArrayList<>(modulepathElements);
537         modulePaths.add(new File(javaHome, JMODS).getAbsolutePath());
538         command.createArg()
539                 .setValue(
540                         "--module-path=" + getPlatformSeparatedList(modulePaths).replace("\\", "\\\\"));
541 
542         if (targetPlatform != null) {
543             command.createArg().setValue("--target-platform=" + targetPlatform);
544         }
545 
546         if (warnIfResolved != null) {
547             command.createArg().setValue("--warn-if-resolved=" + warnIfResolved);
548         }
549 
550         if (doNotResolveByDefault) {
551             command.createArg().setValue("--do-not-resolve-by-default");
552         }
553 
554         command.createArg().setValue(resultingJModFile.getAbsolutePath());
555 
556         return command;
557     }
558 
559     private boolean isConfigurationDefinedInPOM(List<String> configuration) {
560         return configuration != null && !configuration.isEmpty();
561     }
562 
563     private List<String> handleConfigurationListWithDefault(List<String> configuration, String defaultLocation) {
564         List<String> commands = new ArrayList<String>();
565         if (isConfigurationDefinedInPOM(configuration)) {
566             commands.addAll(configuration);
567         } else {
568             if (doDefaultsExist(defaultLocation)) {
569                 commands.add(defaultLocation);
570             }
571         }
572 
573         commands = resolveAgainstProjectBaseDir(commands);
574         return commands;
575     }
576 
577     private List<String> resolveAgainstProjectBaseDir(List<String> relativeDirectories) {
578         List<String> result = new ArrayList<>();
579 
580         for (String configLocation : relativeDirectories) {
581             File dir = new File(getProject().getBasedir(), configLocation);
582             result.add(dir.getAbsolutePath());
583         }
584         return result;
585     }
586 
587     private boolean doDefaultsExist(String defaultLocation) {
588         boolean result = false;
589         File dir = new File(getProject().getBasedir(), defaultLocation);
590         if (dir.exists() && dir.isDirectory()) {
591             result = true;
592         }
593         return result;
594     }
595 
596     private String getPlatformSeparatedList(Collection<String> paths) {
597         StringBuilder sb = new StringBuilder();
598         for (String module : paths) {
599             if (sb.length() > 0) {
600                 sb.append(File.pathSeparatorChar);
601             }
602             sb.append(module);
603         }
604         return sb.toString();
605     }
606 
607     private void writeBoxedWarning(String message) {
608         String line = StringUtils.repeat("*", message.length() + 4);
609         getLog().warn(line);
610         getLog().warn("* " + MessageUtils.buffer().strong(message) + " *");
611         getLog().warn(line);
612     }
613 }