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.jlink;
20  
21  /*
22   * Licensed to the Apache Software Foundation (ASF) under one
23   * or more contributor license agreements.  See the NOTICE file
24   * distributed with this work for additional information
25   * regarding copyright ownership.  The ASF licenses this file
26   * to you under the Apache License, Version 2.0 (the
27   * "License"); you may not use this file except in compliance
28   * with the License.  You may obtain a copy of the License at
29   *
30   *   http://www.apache.org/licenses/LICENSE-2.0
31   *
32   * Unless required by applicable law or agreed to in writing,
33   * software distributed under the License is distributed on an
34   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
35   * KIND, either express or implied.  See the License for the
36   * specific language governing permissions and limitations
37   * under the License.
38   */
39  
40  import javax.inject.Inject;
41  
42  import java.io.File;
43  import java.io.IOException;
44  import java.nio.file.attribute.FileTime;
45  import java.time.Instant;
46  import java.util.ArrayList;
47  import java.util.Collection;
48  import java.util.Collections;
49  import java.util.HashMap;
50  import java.util.List;
51  import java.util.Map;
52  import java.util.Map.Entry;
53  import java.util.NoSuchElementException;
54  import java.util.Optional;
55  
56  import org.apache.commons.io.FileUtils;
57  import org.apache.maven.archiver.MavenArchiver;
58  import org.apache.maven.artifact.Artifact;
59  import org.apache.maven.model.Resource;
60  import org.apache.maven.plugin.MojoExecutionException;
61  import org.apache.maven.plugin.MojoFailureException;
62  import org.apache.maven.plugins.annotations.LifecyclePhase;
63  import org.apache.maven.plugins.annotations.Mojo;
64  import org.apache.maven.plugins.annotations.Parameter;
65  import org.apache.maven.plugins.annotations.ResolutionScope;
66  import org.apache.maven.project.MavenProject;
67  import org.apache.maven.project.MavenProjectHelper;
68  import org.apache.maven.shared.filtering.MavenFilteringException;
69  import org.apache.maven.shared.filtering.MavenResourcesExecution;
70  import org.apache.maven.shared.filtering.MavenResourcesFiltering;
71  import org.apache.maven.toolchain.Toolchain;
72  import org.apache.maven.toolchain.ToolchainManager;
73  import org.apache.maven.toolchain.ToolchainPrivate;
74  import org.apache.maven.toolchain.java.JavaToolchainImpl;
75  import org.codehaus.plexus.archiver.ArchiverException;
76  import org.codehaus.plexus.archiver.zip.ZipArchiver;
77  import org.codehaus.plexus.languages.java.jpms.JavaModuleDescriptor;
78  import org.codehaus.plexus.languages.java.jpms.LocationManager;
79  import org.codehaus.plexus.languages.java.jpms.ResolvePathsRequest;
80  import org.codehaus.plexus.languages.java.jpms.ResolvePathsResult;
81  import org.codehaus.plexus.languages.java.version.JavaVersion;
82  
83  import static java.util.Collections.singletonMap;
84  
85  /**
86   * The JLink goal is intended to create a Java Run Time Image file based on
87   * <a href="https://openjdk.java.net/jeps/282">https://openjdk.java.net/jeps/282</a>,
88   * <a href="https://openjdk.java.net/jeps/220">https://openjdk.java.net/jeps/220</a>.
89   *
90   * @author Karl Heinz Marbaise <a href="mailto:khmarbaise@apache.org">khmarbaise@apache.org</a>
91   */
92  @Mojo(name = "jlink", requiresDependencyResolution = ResolutionScope.RUNTIME, defaultPhase = LifecyclePhase.PACKAGE)
93  public class JLinkMojo extends AbstractJLinkMojo {
94  
95      /**
96       * <p>
97       * Specify the requirements for this jdk toolchain. This overrules the toolchain selected by the
98       * maven-toolchain-plugin.
99       * </p>
100      * <strong>note:</strong> requires at least Maven 3.3.1.
101      */
102     @Parameter
103     private Map<String, String> jdkToolchain;
104 
105     /**
106      * This is intended to strip debug information out. The command line equivalent of <code>jlink</code> is:
107      * <code>-G, --strip-debug</code> strip debug information.
108      */
109     @Parameter(defaultValue = "false")
110     private boolean stripDebug;
111 
112     /**
113      * Here you can define the compression of the resources being used. The command line equivalent is:
114      * <code>-c, --compress=&lt;level&gt;</code>.
115      *
116      * <p>The valid values for the level depend on the JDK:</p>
117      *
118      * <p>For JDK 9+:</p>
119      * <ul>
120      *     <li>0:  No compression. Equivalent to zip-0.</li>
121      *     <li>1:  Constant String Sharing</li>
122      *     <li>2:  Equivalent to zip-6.</li>
123      * </ul>
124      *
125      * <p>For JDK 21+, those values are deprecated and to be removed in a future version.
126      * The supported values are:<br>
127      * {@code zip-[0-9]}, where {@code zip-0} provides no compression,
128      * and {@code zip-9} provides the best compression.<br>
129      * Default is {@code zip-6}.</p>
130      */
131     @Parameter
132     private String compress;
133 
134     /**
135      * Should the plugin generate a launcher script by means of jlink? The command line equivalent is:
136      * <code>--launcher &lt;name&gt;=&lt;module&gt;[/&lt;mainclass&gt;]</code>. The valid values for the level are:
137      * <code>&lt;name&gt;=&lt;module&gt;[/&lt;mainclass&gt;]</code>.
138      */
139     @Parameter
140     private String launcher;
141 
142     /**
143      * Specify one or more launchers for jlink.
144      * The command line equivalent is:
145      * <code>--launcher &lt;name&gt;=&lt;module&gt;[/&lt;mainclass&gt;]</code>.
146      * The valid values are a list of
147      * <code>&lt;name&gt;=&lt;module&gt;[/&lt;mainclass&gt;]</code> terms,
148      * separated by commas.
149      */
150     @Parameter
151     private List<String> launchers;
152 
153     /**
154      * These JVM arguments will be appended to the {@code lib/modules} file.<br>
155      * <strong>This parameter requires at least JDK 14.<br></strong>
156      *
157      * <p>The command line equivalent is: {@code jlink --add-options="..."}.</p>
158      *
159      * <p>Example:</p>
160      *
161      * <pre>
162      *   &lt;addOptions&gt;
163      *     &lt;addOption&gt;-Xmx256m&lt;/addOption&gt;
164      *     &lt;addOption&gt;--enable-preview&lt;/addOption&gt;
165      *     &lt;addOption&gt;-Dvar=value&lt;/addOption&gt;
166      *   &lt;/addOptions&gt;
167      * </pre>
168      *
169      * <p>Above example will result in {@code jlink --add-options="-Xmx256m" --enable-preview -Dvar=value"}.</p>
170      */
171     @Parameter
172     private List<String> addOptions;
173 
174     /**
175      * Limit the universe of observable modules. The following gives an example of the configuration which can be used
176      * in the <code>pom.xml</code> file.
177      *
178      * <pre>
179      *   &lt;limitModules&gt;
180      *     &lt;limitModule&gt;mod1&lt;/limitModule&gt;
181      *     &lt;limitModule&gt;xyz&lt;/limitModule&gt;
182      *     .
183      *     .
184      *   &lt;/limitModules&gt;
185      * </pre>
186      *
187      * This configuration is the equivalent of the command line option:
188      * <code>--limit-modules &lt;mod&gt;[,&lt;mod&gt;...]</code>
189      */
190     @Parameter
191     private List<String> limitModules;
192 
193     /**
194      * <p>
195      * Usually this is not necessary, cause this is handled automatically by the given dependencies.
196      * </p>
197      * <p>
198      * By using the --add-modules you can define the root modules to be resolved. The configuration in
199      * <code>pom.xml</code> file can look like this:
200      * </p>
201      *
202      * <pre>
203      * &lt;addModules&gt;
204      *   &lt;addModule&gt;mod1&lt;/addModule&gt;
205      *   &lt;addModule&gt;first&lt;/addModule&gt;
206      *   .
207      *   .
208      * &lt;/addModules&gt;
209      * </pre>
210      *
211      * The command line equivalent for jlink is: <code>--add-modules &lt;mod&gt;[,&lt;mod&gt;...]</code>.
212      */
213     @Parameter
214     private List<String> addModules;
215 
216     /**
217      * Define the plugin module path to be used. There can be defined multiple entries separated by either {@code ;} or
218      * {@code :}. The jlink command line equivalent is: <code>--plugin-module-path &lt;modulepath&gt;</code>
219      */
220     @Parameter
221     private String pluginModulePath;
222 
223     /**
224      * The output directory for the resulting Run Time Image. The created Run Time Image is stored in non compressed
225      * form. This will later being packaged into a <code>zip</code> file. <code>--output &lt;path&gt;</code>
226      *
227      * <p>The {@link #classifier} is appended as a subdirecty if it exists,
228      * otherwise {@code default} will be used as subdirectory.
229      * This ensures that multiple executions using classifiers will not overwrite the previous run’s image.</p>
230      */
231     @Parameter(defaultValue = "${project.build.directory}/maven-jlink", required = true, readonly = true)
232     private File outputDirectoryImage;
233 
234     @Parameter(defaultValue = "${project.build.directory}", required = true, readonly = true)
235     private File buildDirectory;
236 
237     @Parameter(defaultValue = "${project.build.outputDirectory}", required = true, readonly = true)
238     private File outputDirectory;
239 
240     /**
241      * The byte order of the generated Java Run Time image. <code>--endian &lt;little|big&gt;</code>. If the endian is
242      * not given the default is: <code>native</code>.
243      */
244     // TODO: Should we define either little or big as default? or should we leave it as is?
245     @Parameter
246     private String endian;
247 
248     /**
249      * Include additional paths on the <code>--module-path</code> option. Project dependencies and JDK modules are
250      * automatically added.
251      */
252     @Parameter
253     private List<String> modulePaths;
254 
255     /**
256      * Add the option <code>--bind-services</code> or not.
257      */
258     @Parameter(defaultValue = "false")
259     private boolean bindServices;
260 
261     /**
262      * You can disable a plugin by using this option. <code>--disable-plugin pluginName</code>.
263      */
264     @Parameter
265     private String disablePlugin;
266 
267     /**
268      * <code>--ignore-signing-information</code>
269      */
270     @Parameter(defaultValue = "false")
271     private boolean ignoreSigningInformation;
272 
273     /**
274      * This will omit an <code>includes</code> directory from the resulting Java Run Time Image. The JLink
275      * command line equivalent is: <code>--no-header-files</code>
276      */
277     @Parameter(defaultValue = "false")
278     private boolean noHeaderFiles;
279 
280     /**
281      * This will omit the <code>man</code> directory from the resulting Java Run Time Image. The JLink command
282      * line equivalent is: <code>--no-man-pages</code>
283      */
284     @Parameter(defaultValue = "false")
285     private boolean noManPages;
286 
287     /**
288      * Suggest providers that implement the given service types from the module path.
289      *
290      * <pre>
291      * &lt;suggestProviders&gt;
292      *   &lt;suggestProvider&gt;name-a&lt;/suggestProvider&gt;
293      *   &lt;suggestProvider&gt;name-b&lt;/suggestProvider&gt;
294      *   .
295      *   .
296      * &lt;/suggestProviders&gt;
297      * </pre>
298      *
299      * The jlink command linke equivalent: <code>--suggest-providers [&lt;name&gt;,...]</code>
300      */
301     @Parameter
302     private List<String> suggestProviders;
303 
304     /**
305      * Includes the list of locales where langtag is a BCP 47 language tag.
306      *
307      * <p>This option supports locale matching as defined in RFC 4647.
308      * Ensure that you add the module jdk.localedata when using this option.</p>
309      *
310      * <p>The command line equivalent is: <code>--include-locales=en,ja,*-IN</code>.</p>
311      *
312      * <pre>
313      * &lt;includeLocales&gt;
314      *   &lt;includeLocale&gt;en&lt;/includeLocale&gt;
315      *   &lt;includeLocale&gt;ja&lt;/includeLocale&gt;
316      *   &lt;includeLocale&gt;*-IN&lt;/includeLocale&gt;
317      *   .
318      *   .
319      * &lt;/includeLocales&gt;
320      * </pre>
321      */
322     @Parameter
323     private List<String> includeLocales;
324 
325     /**
326      * This will turn on verbose mode. The jlink command line equivalent is: <code>--verbose</code>
327      */
328     @Parameter(defaultValue = "false")
329     private boolean verbose;
330 
331     /**
332      * Set the JDK location to create a Java custom runtime image.
333      */
334     @Parameter
335     private File sourceJdkModules;
336 
337     /**
338      * Controls whether the plugin tries to attach the resulting artifact to the project.
339      *
340      * @since 3.2.1
341      */
342     @Parameter(defaultValue = "true")
343     private boolean attach;
344 
345     /**
346      * Classifier to add to the artifact generated. If given, the artifact will be attached
347      * as a supplemental artifact.
348      * If not given, this will create the main artifact which is the default behavior.
349      * If you try to do that a second time without using a classifier, the build will fail.
350      */
351     @Parameter
352     private String classifier;
353 
354     /**
355      * Name of the generated ZIP file in the <code>target</code> directory. This will not change the name of the
356      * installed/deployed file.
357      */
358     @Parameter(defaultValue = "${project.build.finalName}", readonly = true)
359     private String finalName;
360 
361     /**
362      * Timestamp for reproducible output archive entries, either formatted as ISO 8601
363      * <code>yyyy-MM-dd'T'HH:mm:ssXXX</code> or as an int representing seconds since the epoch (like
364      * <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>).
365      *
366      * @since 3.2.0
367      */
368     @Parameter(defaultValue = "${project.build.outputTimestamp}")
369     private String outputTimestamp;
370 
371     /**
372      * These files are added to the image after calling jlink, but before creating the zip file.
373      *
374      * @since 3.2.0
375      */
376     @Parameter
377     private List<Resource> additionalResources;
378 
379     /**
380      * Add directory prefix to all of zip entries in top level files/directories.
381      *
382      * <p>For example, if this value is set to <code>prefix</code>,
383      * <code>bin/launcher</code> is transformed to <code>prefix/bin/launcher</code>.</p>
384      *
385      * <p>Empty String is set by default. It means no prefix.</p>
386      *
387      * @since 3.2.1
388      */
389     @Parameter(defaultValue = "")
390     private String zipDirPrefix;
391 
392     /**
393      * Convenience interface for plugins to add or replace artifacts and resources on projects.
394      */
395     private final MavenProjectHelper projectHelper;
396 
397     private final MavenResourcesFiltering mavenResourcesFiltering;
398 
399     private final LocationManager locationManager;
400 
401     /**
402      * The JAR archiver needed for archiving the environments.
403      */
404     private final ZipArchiver zipArchiver = new ZipArchiver();
405 
406     @Inject
407     public JLinkMojo(
408             MavenProjectHelper projectHelper,
409             ToolchainManager toolchainManager,
410             MavenResourcesFiltering mavenResourcesFiltering,
411             LocationManager locationManager) {
412         super(toolchainManager);
413         this.mavenResourcesFiltering = mavenResourcesFiltering;
414         this.projectHelper = projectHelper;
415         this.locationManager = locationManager;
416     }
417 
418     @Override
419     public void execute() throws MojoExecutionException, MojoFailureException {
420         failIfParametersAreNotInTheirValidValueRanges();
421 
422         setOutputDirectoryImage();
423 
424         ifOutputDirectoryExistsDelteIt();
425 
426         JLinkExecutor jLinkExec = getJlinkExecutor();
427         Collection<String> modulesToAdd = new ArrayList<>();
428         if (addModules != null) {
429             modulesToAdd.addAll(addModules);
430         }
431         jLinkExec.addAllModules(modulesToAdd);
432 
433         Collection<String> pathsOfModules = new ArrayList<>();
434         if (modulePaths != null) {
435             pathsOfModules.addAll(modulePaths);
436         }
437 
438         for (Entry<String, File> item : getModulePathElements().entrySet()) {
439             getLog().info(" -> module: " + item.getKey() + " ( "
440                     + item.getValue().getPath() + " )");
441 
442             // We use the real module name and not the artifact Id...
443             modulesToAdd.add(item.getKey());
444             pathsOfModules.add(item.getValue().getPath());
445         }
446 
447         // The jmods directory of the JDK
448         jLinkExec
449                 .getJmodsFolder(this.sourceJdkModules)
450                 .ifPresent(jmodsFolder -> pathsOfModules.add(jmodsFolder.getAbsolutePath()));
451         jLinkExec.addAllModulePaths(pathsOfModules);
452 
453         List<String> jlinkArgs = createJlinkArgs(pathsOfModules, modulesToAdd);
454 
455         try {
456             jLinkExec.executeJlink(jlinkArgs);
457         } catch (IllegalStateException e) {
458             throw new MojoFailureException("Unable to find jlink command: " + e.getMessage(), e);
459         }
460 
461         // Add additional resources
462         try {
463             mavenResourcesFiltering.filterResources(new MavenResourcesExecution(
464                     additionalResources,
465                     outputDirectoryImage,
466                     getProject(),
467                     "UTF-8",
468                     Collections.emptyList(),
469                     Collections.emptyList(),
470                     getSession()));
471         } catch (MavenFilteringException e) {
472             throw new MojoFailureException("Unable to copy the additional resources: " + e.getMessage(), e);
473         }
474 
475         File createZipArchiveFromImage = createZipArchiveFromImage(buildDirectory, outputDirectoryImage);
476 
477         attachArtifactUnlessDisabled(createZipArchiveFromImage);
478     }
479 
480     /**
481      * Gets the compile classpath elements while filtering out artifacts that should be skipped.
482      *
483      * @param project the Maven project
484      * @return list of files that should be included in the classpath
485      */
486     List<File> getCompileClasspathElements(MavenProject project) {
487         List<File> list = new ArrayList<>(project.getArtifacts().size() + 1);
488 
489         for (Artifact artifact : project.getArtifacts()) {
490             boolean shouldSkip = shouldSkip(artifact);
491             getLog().debug("Adding artifact: " + artifact.getGroupId() + ":" + artifact.getArtifactId() + ":"
492                     + artifact.getVersion() + (shouldSkip ? " (skipping)" : ""));
493             if (!shouldSkip) {
494                 list.add(artifact.getFile());
495             }
496         }
497         return list;
498     }
499 
500     /**
501      * Determines if an artifact should be skipped based on its properties.
502      * Currently, skips POM type artifacts, but can be extended for other cases.
503      *
504      * @param artifact the artifact to check
505      * @return true if the artifact should be skipped, false otherwise
506      */
507     private boolean shouldSkip(Artifact artifact) {
508         return "pom".equals(artifact.getType());
509     }
510 
511     Map<String, File> getModulePathElements() throws MojoFailureException {
512         // For now only allow named modules. Once we can create a graph with ASM we can specify exactly the modules
513         // and we can detect if auto modules are used. In that case, MavenProject.setFile() should not be used, so
514         // you cannot depend on this project and so it won't be distributed.
515 
516         Map<String, File> modulepathElements = new HashMap<>();
517 
518         try {
519             Collection<File> dependencyArtifacts = getCompileClasspathElements(getProject());
520 
521             ResolvePathsRequest<File> request = ResolvePathsRequest.ofFiles(dependencyArtifacts);
522 
523             Optional<Toolchain> toolchain = getToolchain();
524             if (toolchain.isPresent()
525                     && toolchain.orElseThrow(NoSuchElementException::new) instanceof JavaToolchainImpl) {
526                 Toolchain toolchain1 = toolchain.orElseThrow(NoSuchElementException::new);
527                 request.setJdkHome(new File(((JavaToolchainImpl) toolchain1).getJavaHome()));
528             }
529 
530             ResolvePathsResult<File> resolvePathsResult = locationManager.resolvePaths(request);
531 
532             for (Map.Entry<File, JavaModuleDescriptor> entry :
533                     resolvePathsResult.getPathElements().entrySet()) {
534                 JavaModuleDescriptor descriptor = entry.getValue();
535                 if (descriptor == null) {
536                     String message = "The given dependency " + entry.getKey()
537                             + " does not have a module-info.java file. So it can't be linked.";
538                     getLog().error(message);
539                     throw new MojoFailureException(message);
540                 }
541 
542                 // Filter out automatic modules
543                 if (descriptor.isAutomatic()) {
544                     getLog().debug("Ignoring automatic module: " + descriptor.name());
545                     continue;
546                 }
547 
548                 if (modulepathElements.containsKey(descriptor.name())) {
549                     getLog().warn("The module name " + descriptor.name() + " does already exists.");
550                 }
551                 modulepathElements.put(descriptor.name(), entry.getKey());
552             }
553 
554             // This part is for the module in target/classes ? (Hacky..)
555             // FIXME: Is there a better way to identify that code exists?
556             if (outputDirectory.exists()) {
557                 List<File> singletonList = Collections.singletonList(outputDirectory);
558 
559                 ResolvePathsRequest<File> singleModuls = ResolvePathsRequest.ofFiles(singletonList);
560 
561                 ResolvePathsResult<File> resolvePaths = locationManager.resolvePaths(singleModuls);
562                 for (Entry<File, JavaModuleDescriptor> entry :
563                         resolvePaths.getPathElements().entrySet()) {
564                     JavaModuleDescriptor descriptor = entry.getValue();
565                     if (descriptor == null) {
566                         String message = "The given project " + entry.getKey()
567                                 + " does not contain a module-info.java file. So it can't be linked.";
568                         getLog().error(message);
569                         throw new MojoFailureException(message);
570                     }
571 
572                     if (descriptor.isAutomatic()) {
573                         getLog().debug("Ignoring automatic module: " + descriptor.name());
574                         continue;
575                     }
576 
577                     if (modulepathElements.containsKey(descriptor.name())) {
578                         getLog().warn("The module name " + descriptor.name() + " does already exists.");
579                     }
580                     modulepathElements.put(descriptor.name(), entry.getKey());
581                 }
582             }
583 
584         } catch (IOException e) {
585             getLog().error(e.getMessage());
586             throw new MojoFailureException(e.getMessage());
587         }
588 
589         return modulepathElements;
590     }
591 
592     private boolean projectHasAlreadySetAnArtifact() {
593         if (getProject().getArtifact().getFile() != null) {
594             return getProject().getArtifact().getFile().isFile();
595         } else {
596             return false;
597         }
598     }
599 
600     /**
601      * @return true when the classifier is not {@code null} and not empty
602      */
603     private boolean hasClassifier() {
604         return hasClassifier(getClassifier());
605     }
606 
607     private File createZipArchiveFromImage(File outputDirectory, File outputDirectoryImage)
608             throws MojoExecutionException {
609         if (zipDirPrefix != null && !zipDirPrefix.isEmpty() && !zipDirPrefix.endsWith("/")) {
610             zipDirPrefix += "/";
611         }
612         zipArchiver.addDirectory(outputDirectoryImage, zipDirPrefix);
613 
614         // configure for Reproducible Builds based on outputTimestamp value
615         Optional<Instant> lastModified = MavenArchiver.parseBuildOutputTimestamp(outputTimestamp);
616         if (lastModified.isPresent()) {
617             zipArchiver.configureReproducibleBuild(FileTime.from(lastModified.get()));
618         }
619 
620         File resultArchive = getZipFile(outputDirectory, finalName, getClassifier());
621 
622         zipArchiver.setDestFile(resultArchive);
623         try {
624             zipArchiver.createArchive();
625         } catch (ArchiverException | IOException e) {
626             getLog().error(e.getMessage(), e);
627             throw new MojoExecutionException(e.getMessage(), e);
628         }
629 
630         return resultArchive;
631     }
632 
633     private void attachArtifactUnlessDisabled(File artifactFile) throws MojoExecutionException {
634         if (!attach) {
635             return;
636         }
637 
638         if (hasClassifier()) {
639             projectHelper.attachArtifact(getProject(), "jlink", getClassifier(), artifactFile);
640         } else {
641             if (projectHasAlreadySetAnArtifact()) {
642                 throw new MojoExecutionException("You have to use a classifier "
643                         + "to attach supplemental artifacts to the project instead of replacing them.");
644             }
645             getProject().getArtifact().setFile(artifactFile);
646         }
647     }
648 
649     private void failIfParametersAreNotInTheirValidValueRanges() throws MojoFailureException {
650         if (endian != null && (!"big".equals(endian) && !"little".equals(endian))) {
651             String message = "The given endian parameter " + endian
652                     + " does not contain one of the following values: 'little' or 'big'.";
653             getLog().error(message);
654             throw new MojoFailureException(message);
655         }
656 
657         if (addOptions != null && !addOptions.isEmpty()) {
658             requireJdk14();
659         }
660     }
661 
662     private void requireJdk14() throws MojoFailureException {
663         // needs JDK 14+
664         Optional<Toolchain> optToolchain = getToolchain();
665         String java14reqMsg = "parameter 'addOptions' needs at least a Java 14 runtime or a Java 14 toolchain.";
666 
667         if (optToolchain.isPresent()) {
668             Toolchain toolchain = optToolchain.orElseThrow(NoSuchElementException::new);
669             if (!(toolchain instanceof ToolchainPrivate)) {
670                 getLog().warn("Unable to check toolchain java version.");
671                 return;
672             }
673             ToolchainPrivate toolchainPrivate = (ToolchainPrivate) toolchain;
674             if (!toolchainPrivate.matchesRequirements(singletonMap("jdk", "14"))) {
675                 throw new MojoFailureException(java14reqMsg);
676             }
677         } else if (!JavaVersion.JAVA_VERSION.isAtLeast("14")) {
678             throw new MojoFailureException(java14reqMsg);
679         }
680     }
681 
682     /**
683      * Use a separate directory for each image.
684      *
685      * <p>Rationale: If a user creates multiple jlink artifacts using classifiers,
686      * the directories should not overwrite themselves for each execution.</p>
687      */
688     private void setOutputDirectoryImage() {
689         if (hasClassifier()) {
690             final File classifiersDirectory = new File(outputDirectoryImage, "classifiers");
691             outputDirectoryImage = new File(classifiersDirectory, classifier);
692         } else {
693             outputDirectoryImage = new File(outputDirectoryImage, "default");
694         }
695     }
696 
697     private void ifOutputDirectoryExistsDelteIt() throws MojoExecutionException {
698         if (outputDirectoryImage.exists()) {
699             // Delete the output folder of JLink before we start
700             // otherwise JLink will fail with a message "Error: directory already exists: ..."
701             try {
702                 getLog().debug("Deleting existing " + outputDirectoryImage.getAbsolutePath());
703                 FileUtils.forceDelete(outputDirectoryImage);
704             } catch (IOException e) {
705                 getLog().error("IOException", e);
706                 throw new MojoExecutionException(
707                         "Failure during deletion of " + outputDirectoryImage.getAbsolutePath() + " occured.");
708             }
709         }
710     }
711 
712     protected List<String> createJlinkArgs(Collection<String> pathsOfModules, Collection<String> modulesToAdd)
713             throws MojoExecutionException {
714         List<String> jlinkArgs = new ArrayList<>();
715 
716         if (stripDebug) {
717             jlinkArgs.add("--strip-debug");
718         }
719 
720         if (bindServices) {
721             jlinkArgs.add("--bind-services");
722         }
723 
724         if (endian != null) {
725             jlinkArgs.add("--endian");
726             jlinkArgs.add(endian);
727         }
728         if (ignoreSigningInformation) {
729             jlinkArgs.add("--ignore-signing-information");
730         }
731         if (compress != null) {
732             jlinkArgs.add("--compress");
733             jlinkArgs.add(compress);
734         }
735         if (launcher != null) {
736             if (launchers != null) {
737                 throw new MojoExecutionException("Specify either single <launcher> or multiple <launchers>, not both.");
738             } else {
739                 launchers = List.of(launcher);
740             }
741         }
742         if (launchers != null) {
743             for (String item : launchers) {
744                 jlinkArgs.add("--launcher");
745                 jlinkArgs.add(item);
746             }
747         }
748         if (addOptions != null && !addOptions.isEmpty()) {
749             jlinkArgs.add("--add-options=" + String.join(" ", addOptions));
750         }
751 
752         if (disablePlugin != null) {
753             jlinkArgs.add("--disable-plugin");
754             jlinkArgs.add(disablePlugin);
755         }
756         if (pathsOfModules != null && !pathsOfModules.isEmpty()) {
757             // @formatter:off
758             jlinkArgs.add("--module-path");
759             jlinkArgs.add(String.join(File.pathSeparator, pathsOfModules).replace("\\", "\\\\"));
760             // @formatter:off
761         }
762 
763         if (noHeaderFiles) {
764             jlinkArgs.add("--no-header-files");
765         }
766 
767         if (noManPages) {
768             jlinkArgs.add("--no-man-pages");
769         }
770 
771         if (hasLimitModules()) {
772             jlinkArgs.add("--limit-modules");
773             String sb = String.join(",", limitModules);
774             jlinkArgs.add(sb);
775         }
776 
777         if (!modulesToAdd.isEmpty()) {
778             jlinkArgs.add("--add-modules");
779             // This must be name of the module and *NOT* the name of the
780             // file! Can we somehow pre check this information to fail early?
781             String sb = String.join(",", modulesToAdd);
782             jlinkArgs.add(sb.replace("\\", "\\\\"));
783         }
784 
785         if (hasIncludeLocales()) {
786             jlinkArgs.add("--add-modules");
787             jlinkArgs.add("jdk.localedata");
788             jlinkArgs.add("--include-locales");
789             String sb = String.join(",", includeLocales);
790             jlinkArgs.add(sb);
791         }
792 
793         if (pluginModulePath != null) {
794             jlinkArgs.add("--plugin-module-path");
795             StringBuilder sb = convertSeparatedModulePathToPlatformSeparatedModulePath(pluginModulePath);
796             jlinkArgs.add(sb.toString().replace("\\", "\\\\"));
797         }
798 
799         if (buildDirectory != null) {
800             jlinkArgs.add("--output");
801             jlinkArgs.add(outputDirectoryImage.getAbsolutePath());
802         }
803 
804         if (verbose) {
805             jlinkArgs.add("--verbose");
806         }
807 
808         // NOTE: suggestProviders is a terminal JlinkTask, so must be at the end!
809         if (hasSuggestProviders()) {
810             jlinkArgs.add("--suggest-providers");
811             String sb = String.join(",", suggestProviders);
812             jlinkArgs.add(sb);
813         }
814 
815         return Collections.unmodifiableList(jlinkArgs);
816     }
817 
818     private boolean hasIncludeLocales() {
819         return includeLocales != null && !includeLocales.isEmpty();
820     }
821 
822     private boolean hasSuggestProviders() {
823         return suggestProviders != null && !suggestProviders.isEmpty();
824     }
825 
826     private boolean hasLimitModules() {
827         return limitModules != null && !limitModules.isEmpty();
828     }
829 
830     /**
831      * {@inheritDoc}
832      */
833     @Override
834     protected String getClassifier() {
835         return classifier;
836     }
837 
838     /**
839      * Returns the archive file to generate, based on an optional classifier.
840      *
841      * @param basedir the output directory
842      * @param finalName the name of the zip file
843      * @param classifier an optional classifier
844      * @return the file to generate
845      */
846     private static File getZipFile(File basedir, String finalName, String classifier) {
847         if (finalName.isEmpty()) {
848             throw new IllegalArgumentException("finalName is not allowed to be empty.");
849         }
850 
851         StringBuilder fileName = new StringBuilder(finalName);
852 
853         if (hasClassifier(classifier)) {
854             fileName.append("-").append(classifier);
855         }
856 
857         fileName.append(".zip");
858 
859         return new File(basedir, fileName.toString());
860     }
861 
862     private static boolean hasClassifier(String classifier) {
863         boolean result = false;
864         if (classifier != null && !classifier.isEmpty()) {
865             result = true;
866         }
867 
868         return result;
869     }
870 }