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 java.io.File;
41  import java.io.IOException;
42  import java.util.ArrayList;
43  import java.util.Collection;
44  import java.util.Collections;
45  import java.util.Date;
46  import java.util.HashMap;
47  import java.util.List;
48  import java.util.Map;
49  import java.util.Map.Entry;
50  import java.util.NoSuchElementException;
51  import java.util.Optional;
52  
53  import org.apache.commons.io.FileUtils;
54  import org.apache.maven.archiver.MavenArchiver;
55  import org.apache.maven.artifact.Artifact;
56  import org.apache.maven.plugin.MojoExecutionException;
57  import org.apache.maven.plugin.MojoFailureException;
58  import org.apache.maven.plugins.annotations.Component;
59  import org.apache.maven.plugins.annotations.LifecyclePhase;
60  import org.apache.maven.plugins.annotations.Mojo;
61  import org.apache.maven.plugins.annotations.Parameter;
62  import org.apache.maven.plugins.annotations.ResolutionScope;
63  import org.apache.maven.project.MavenProject;
64  import org.apache.maven.project.MavenProjectHelper;
65  import org.apache.maven.toolchain.Toolchain;
66  import org.apache.maven.toolchain.ToolchainPrivate;
67  import org.apache.maven.toolchain.java.DefaultJavaToolChain;
68  import org.codehaus.plexus.archiver.Archiver;
69  import org.codehaus.plexus.archiver.ArchiverException;
70  import org.codehaus.plexus.archiver.zip.ZipArchiver;
71  import org.codehaus.plexus.languages.java.jpms.JavaModuleDescriptor;
72  import org.codehaus.plexus.languages.java.jpms.LocationManager;
73  import org.codehaus.plexus.languages.java.jpms.ResolvePathsRequest;
74  import org.codehaus.plexus.languages.java.jpms.ResolvePathsResult;
75  import org.codehaus.plexus.languages.java.version.JavaVersion;
76  
77  import static java.util.Collections.singletonMap;
78  
79  /**
80   * The JLink goal is intended to create a Java Run Time Image file based on
81   * <a href="https://openjdk.java.net/jeps/282">https://openjdk.java.net/jeps/282</a>,
82   * <a href="https://openjdk.java.net/jeps/220">https://openjdk.java.net/jeps/220</a>.
83   *
84   * @author Karl Heinz Marbaise <a href="mailto:khmarbaise@apache.org">khmarbaise@apache.org</a>
85   */
86  @Mojo(name = "jlink", requiresDependencyResolution = ResolutionScope.RUNTIME, defaultPhase = LifecyclePhase.PACKAGE)
87  public class JLinkMojo extends AbstractJLinkMojo {
88      @Component
89      private LocationManager locationManager;
90  
91      /**
92       * <p>
93       * Specify the requirements for this jdk toolchain. This overrules the toolchain selected by the
94       * maven-toolchain-plugin.
95       * </p>
96       * <strong>note:</strong> requires at least Maven 3.3.1
97       */
98      @Parameter
99      private Map<String, String> jdkToolchain;
100 
101     /**
102      * This is intended to strip debug information out. The command line equivalent of <code>jlink</code> is:
103      * <code>-G, --strip-debug</code> strip debug information.
104      */
105     @Parameter(defaultValue = "false")
106     private boolean stripDebug;
107 
108     /**
109      * Here you can define the compression of the resources being used. The command line equivalent is:
110      * <code>-c, --compress=&lt;level&gt;</code>.
111      *
112      * <p>The valid values for the level depend on the JDK:</p>
113      *
114      * <p>For JDK 9+:</p>
115      * <ul>
116      *     <li>0:  No compression. Equivalent to zip-0.</li>
117      *     <li>1:  Constant String Sharing</li>
118      *     <li>2:  Equivalent to zip-6.</li>
119      * </ul>
120      *
121      * <p>For JDK 21+, those values are deprecated and to be removed in a future version.
122      * The supported values are:<br>
123      * {@code zip-[0-9]}, where {@code zip-0} provides no compression,
124      * and {@code zip-9} provides the best compression.<br>
125      * Default is {@code zip-6}.</p>
126      */
127     @Parameter
128     private String compress;
129 
130     /**
131      * Should the plugin generate a launcher script by means of jlink? The command line equivalent is:
132      * <code>--launcher &lt;name&gt;=&lt;module&gt;[/&lt;mainclass&gt;]</code>. The valid values for the level are:
133      * <code>&lt;name&gt;=&lt;module&gt;[/&lt;mainclass&gt;]</code>.
134      */
135     @Parameter
136     private String launcher;
137 
138     /**
139      * These JVM arguments will be appended to the {@code lib/modules} file.<br>
140      * <strong>This parameter requires at least JDK 14.<br></strong>
141      *
142      * <p>The command line equivalent is: {@code jlink --add-options="..."}.</p>
143      *
144      * <p>Example:</p>
145      *
146      * <pre>
147      *   &lt;addOptions&gt;
148      *     &lt;addOption&gt;-Xmx256m&lt;/addOption&gt;
149      *     &lt;addOption&gt;--enable-preview&lt;/addOption&gt;
150      *     &lt;addOption&gt;-Dvar=value&lt;/addOption&gt;
151      *   &lt;/addOptions&gt;
152      * </pre>
153      *
154      * <p>Above example will result in {@code jlink --add-options="-Xmx256m" --enable-preview -Dvar=value"}.</p>
155      */
156     @Parameter
157     private List<String> addOptions;
158 
159     /**
160      * Limit the universe of observable modules. The following gives an example of the configuration which can be used
161      * in the <code>pom.xml</code> file.
162      *
163      * <pre>
164      *   &lt;limitModules&gt;
165      *     &lt;limitModule&gt;mod1&lt;/limitModule&gt;
166      *     &lt;limitModule&gt;xyz&lt;/limitModule&gt;
167      *     .
168      *     .
169      *   &lt;/limitModules&gt;
170      * </pre>
171      *
172      * This configuration is the equivalent of the command line option:
173      * <code>--limit-modules &lt;mod&gt;[,&lt;mod&gt;...]</code>
174      */
175     @Parameter
176     private List<String> limitModules;
177 
178     /**
179      * <p>
180      * Usually this is not necessary, cause this is handled automatically by the given dependencies.
181      * </p>
182      * <p>
183      * By using the --add-modules you can define the root modules to be resolved. The configuration in
184      * <code>pom.xml</code> file can look like this:
185      * </p>
186      *
187      * <pre>
188      * &lt;addModules&gt;
189      *   &lt;addModule&gt;mod1&lt;/addModule&gt;
190      *   &lt;addModule&gt;first&lt;/addModule&gt;
191      *   .
192      *   .
193      * &lt;/addModules&gt;
194      * </pre>
195      *
196      * The command line equivalent for jlink is: <code>--add-modules &lt;mod&gt;[,&lt;mod&gt;...]</code>.
197      */
198     @Parameter
199     private List<String> addModules;
200 
201     /**
202      * Define the plugin module path to be used. There can be defined multiple entries separated by either {@code ;} or
203      * {@code :}. The jlink command line equivalent is: <code>--plugin-module-path &lt;modulepath&gt;</code>
204      */
205     @Parameter
206     private String pluginModulePath;
207 
208     /**
209      * The output directory for the resulting Run Time Image. The created Run Time Image is stored in non compressed
210      * form. This will later being packaged into a <code>zip</code> file. <code>--output &lt;path&gt;</code>
211      *
212      * <p>The {@link #classifier} is appended as a subdirecty if it exists,
213      * otherwise {@code default} will be used as subdirectory.
214      * This ensures that multiple executions using classifiers will not overwrite the previous run’s image.</p>
215      */
216     @Parameter(defaultValue = "${project.build.directory}/maven-jlink", required = true, readonly = true)
217     private File outputDirectoryImage;
218 
219     @Parameter(defaultValue = "${project.build.directory}", required = true, readonly = true)
220     private File buildDirectory;
221 
222     @Parameter(defaultValue = "${project.build.outputDirectory}", required = true, readonly = true)
223     private File outputDirectory;
224 
225     /**
226      * The byte order of the generated Java Run Time image. <code>--endian &lt;little|big&gt;</code>. If the endian is
227      * not given the default is: <code>native</code>.
228      */
229     // TODO: Should we define either little or big as default? or should we left as it.
230     @Parameter
231     private String endian;
232 
233     /**
234      * Include additional paths on the <code>--module-path</code> option. Project dependencies and JDK modules are
235      * automatically added.
236      */
237     @Parameter
238     private List<String> modulePaths;
239 
240     /**
241      * Add the option <code>--bind-services</code> or not.
242      */
243     @Parameter(defaultValue = "false")
244     private boolean bindServices;
245 
246     /**
247      * You can disable a plugin by using this option. <code>--disable-plugin pluginName</code>.
248      */
249     @Parameter
250     private String disablePlugin;
251 
252     /**
253      * <code>--ignore-signing-information</code>
254      */
255     @Parameter(defaultValue = "false")
256     private boolean ignoreSigningInformation;
257 
258     /**
259      * This will suppress to have an <code>includes</code> directory in the resulting Java Run Time Image. The JLink
260      * command line equivalent is: <code>--no-header-files</code>
261      */
262     @Parameter(defaultValue = "false")
263     private boolean noHeaderFiles;
264 
265     /**
266      * This will suppress to have the <code>man</code> directory in the resulting Java Run Time Image. The JLink command
267      * line equivalent is: <code>--no-man-pages</code>
268      */
269     @Parameter(defaultValue = "false")
270     private boolean noManPages;
271 
272     /**
273      * Suggest providers that implement the given service types from the module path.
274      *
275      * <pre>
276      * &lt;suggestProviders&gt;
277      *   &lt;suggestProvider&gt;name-a&lt;/suggestProvider&gt;
278      *   &lt;suggestProvider&gt;name-b&lt;/suggestProvider&gt;
279      *   .
280      *   .
281      * &lt;/suggestProviders&gt;
282      * </pre>
283      *
284      * The jlink command linke equivalent: <code>--suggest-providers [&lt;name&gt;,...]</code>
285      */
286     @Parameter
287     private List<String> suggestProviders;
288 
289     /**
290      * Includes the list of locales where langtag is a BCP 47 language tag.
291      *
292      * <p>This option supports locale matching as defined in RFC 4647.
293      * Ensure that you add the module jdk.localedata when using this option.</p>
294      *
295      * <p>The command line equivalent is: <code>--include-locales=en,ja,*-IN</code>.</p>
296      *
297      * <pre>
298      * &lt;includeLocales&gt;
299      *   &lt;includeLocale&gt;en&lt;/includeLocale&gt;
300      *   &lt;includeLocale&gt;ja&lt;/includeLocale&gt;
301      *   &lt;includeLocale&gt;*-IN&lt;/includeLocale&gt;
302      *   .
303      *   .
304      * &lt;/includeLocales&gt;
305      * </pre>
306      */
307     @Parameter
308     private List<String> includeLocales;
309 
310     /**
311      * This will turn on verbose mode. The jlink command line equivalent is: <code>--verbose</code>
312      */
313     @Parameter(defaultValue = "false")
314     private boolean verbose;
315 
316     /**
317      * The JAR archiver needed for archiving the environments.
318      */
319     @Component(role = Archiver.class, hint = "zip")
320     private ZipArchiver zipArchiver;
321 
322     /**
323      * Set the JDK location to create a Java custom runtime image.
324      */
325     @Parameter
326     private File sourceJdkModules;
327 
328     /**
329      * Classifier to add to the artifact generated. If given, the artifact will be attached
330      * as a supplemental artifact.
331      * If not given this will create the main artifact which is the default behavior.
332      * If you try to do that a second time without using a classifier the build will fail.
333      */
334     @Parameter
335     private String classifier;
336 
337     /**
338      * Name of the generated ZIP file in the <code>target</code> directory. This will not change the name of the
339      * installed/deployed file.
340      */
341     @Parameter(defaultValue = "${project.build.finalName}", readonly = true)
342     private String finalName;
343 
344     /**
345      * Timestamp for reproducible output archive entries, either formatted as ISO 8601
346      * <code>yyyy-MM-dd'T'HH:mm:ssXXX</code> or as an int representing seconds since the epoch (like
347      * <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>).
348      *
349      * @since 3.2.0
350      */
351     @Parameter(defaultValue = "${project.build.outputTimestamp}")
352     private String outputTimestamp;
353 
354     /**
355      * Convenience interface for plugins to add or replace artifacts and resources on projects.
356      */
357     @Component
358     private MavenProjectHelper projectHelper;
359 
360     @Override
361     public void execute() throws MojoExecutionException, MojoFailureException {
362         failIfParametersAreNotInTheirValidValueRanges();
363 
364         setOutputDirectoryImage();
365 
366         ifOutputDirectoryExistsDelteIt();
367 
368         JLinkExecutor jLinkExec = getExecutor();
369         Collection<String> modulesToAdd = new ArrayList<>();
370         if (addModules != null) {
371             modulesToAdd.addAll(addModules);
372         }
373         jLinkExec.addAllModules(modulesToAdd);
374 
375         Collection<String> pathsOfModules = new ArrayList<>();
376         if (modulePaths != null) {
377             pathsOfModules.addAll(modulePaths);
378         }
379 
380         for (Entry<String, File> item : getModulePathElements().entrySet()) {
381             getLog().info(" -> module: " + item.getKey() + " ( "
382                     + item.getValue().getPath() + " )");
383 
384             // We use the real module name and not the artifact Id...
385             modulesToAdd.add(item.getKey());
386             pathsOfModules.add(item.getValue().getPath());
387         }
388 
389         // The jmods directory of the JDK
390         jLinkExec
391                 .getJmodsFolder(this.sourceJdkModules)
392                 .ifPresent(jmodsFolder -> pathsOfModules.add(jmodsFolder.getAbsolutePath()));
393         jLinkExec.addAllModulePaths(pathsOfModules);
394 
395         List<String> jlinkArgs = createJlinkArgs(pathsOfModules, modulesToAdd);
396 
397         try {
398             jLinkExec.executeJlink(jlinkArgs);
399         } catch (IllegalStateException e) {
400             throw new MojoFailureException("Unable to find jlink command: " + e.getMessage(), e);
401         }
402 
403         File createZipArchiveFromImage = createZipArchiveFromImage(buildDirectory, outputDirectoryImage);
404 
405         if (hasClassifier()) {
406             projectHelper.attachArtifact(getProject(), "jlink", getClassifier(), createZipArchiveFromImage);
407         } else {
408             if (projectHasAlreadySetAnArtifact()) {
409                 throw new MojoExecutionException("You have to use a classifier "
410                         + "to attach supplemental artifacts to the project instead of replacing them.");
411             }
412             getProject().getArtifact().setFile(createZipArchiveFromImage);
413         }
414     }
415 
416     private List<File> getCompileClasspathElements(MavenProject project) {
417         List<File> list = new ArrayList<>(project.getArtifacts().size() + 1);
418 
419         for (Artifact a : project.getArtifacts()) {
420             getLog().debug("Artifact: " + a.getGroupId() + ":" + a.getArtifactId() + ":" + a.getVersion());
421             list.add(a.getFile());
422         }
423         return list;
424     }
425 
426     private Map<String, File> getModulePathElements() throws MojoFailureException {
427         // For now only allow named modules. Once we can create a graph with ASM we can specify exactly the modules
428         // and we can detect if auto modules are used. In that case, MavenProject.setFile() should not be used, so
429         // you cannot depend on this project and so it won't be distributed.
430 
431         Map<String, File> modulepathElements = new HashMap<>();
432 
433         try {
434             Collection<File> dependencyArtifacts = getCompileClasspathElements(getProject());
435 
436             ResolvePathsRequest<File> request = ResolvePathsRequest.ofFiles(dependencyArtifacts);
437 
438             Optional<Toolchain> toolchain = getToolchain();
439             if (toolchain.isPresent()
440                     && toolchain.orElseThrow(NoSuchElementException::new) instanceof DefaultJavaToolChain) {
441                 Toolchain toolcahin1 = toolchain.orElseThrow(NoSuchElementException::new);
442                 request.setJdkHome(new File(((DefaultJavaToolChain) toolcahin1).getJavaHome()));
443             }
444 
445             ResolvePathsResult<File> resolvePathsResult = locationManager.resolvePaths(request);
446 
447             for (Map.Entry<File, JavaModuleDescriptor> entry :
448                     resolvePathsResult.getPathElements().entrySet()) {
449                 JavaModuleDescriptor descriptor = entry.getValue();
450                 if (descriptor == null) {
451                     String message = "The given dependency " + entry.getKey()
452                             + " does not have a module-info.java file. So it can't be linked.";
453                     getLog().error(message);
454                     throw new MojoFailureException(message);
455                 }
456 
457                 // Don't warn for automatic modules, let the jlink tool do that
458                 getLog().debug(" module: " + descriptor.name() + " automatic: " + descriptor.isAutomatic());
459                 if (modulepathElements.containsKey(descriptor.name())) {
460                     getLog().warn("The module name " + descriptor.name() + " does already exists.");
461                 }
462                 modulepathElements.put(descriptor.name(), entry.getKey());
463             }
464 
465             // This part is for the module in target/classes ? (Hacky..)
466             // FIXME: Is there a better way to identify that code exists?
467             if (outputDirectory.exists()) {
468                 List<File> singletonList = Collections.singletonList(outputDirectory);
469 
470                 ResolvePathsRequest<File> singleModuls = ResolvePathsRequest.ofFiles(singletonList);
471 
472                 ResolvePathsResult<File> resolvePaths = locationManager.resolvePaths(singleModuls);
473                 for (Entry<File, JavaModuleDescriptor> entry :
474                         resolvePaths.getPathElements().entrySet()) {
475                     JavaModuleDescriptor descriptor = entry.getValue();
476                     if (descriptor == null) {
477                         String message = "The given project " + entry.getKey()
478                                 + " does not contain a module-info.java file. So it can't be linked.";
479                         getLog().error(message);
480                         throw new MojoFailureException(message);
481                     }
482                     if (modulepathElements.containsKey(descriptor.name())) {
483                         getLog().warn("The module name " + descriptor.name() + " does already exists.");
484                     }
485                     modulepathElements.put(descriptor.name(), entry.getKey());
486                 }
487             }
488 
489         } catch (IOException e) {
490             getLog().error(e.getMessage());
491             throw new MojoFailureException(e.getMessage());
492         }
493 
494         return modulepathElements;
495     }
496 
497     private JLinkExecutor getExecutor() {
498         return getJlinkExecutor();
499     }
500 
501     private boolean projectHasAlreadySetAnArtifact() {
502         if (getProject().getArtifact().getFile() != null) {
503             return getProject().getArtifact().getFile().isFile();
504         } else {
505             return false;
506         }
507     }
508 
509     /**
510      * @return true in case where the classifier is not {@code null} and contains something else than white spaces.
511      */
512     protected boolean hasClassifier() {
513         boolean result = false;
514         if (getClassifier() != null && !getClassifier().isEmpty()) {
515             result = true;
516         }
517 
518         return result;
519     }
520 
521     private File createZipArchiveFromImage(File outputDirectory, File outputDirectoryImage)
522             throws MojoExecutionException {
523         zipArchiver.addDirectory(outputDirectoryImage);
524 
525         // configure for Reproducible Builds based on outputTimestamp value
526         Date lastModified = new MavenArchiver().parseOutputTimestamp(outputTimestamp);
527         if (lastModified != null) {
528             zipArchiver.configureReproducible(lastModified);
529         }
530 
531         File resultArchive = getArchiveFile(outputDirectory, finalName, getClassifier(), "zip");
532 
533         zipArchiver.setDestFile(resultArchive);
534         try {
535             zipArchiver.createArchive();
536         } catch (ArchiverException | IOException e) {
537             getLog().error(e.getMessage(), e);
538             throw new MojoExecutionException(e.getMessage(), e);
539         }
540 
541         return resultArchive;
542     }
543 
544     private void failIfParametersAreNotInTheirValidValueRanges() throws MojoFailureException {
545         if (endian != null && (!"big".equals(endian) && !"little".equals(endian))) {
546             String message = "The given endian parameter " + endian
547                     + " does not contain one of the following values: 'little' or 'big'.";
548             getLog().error(message);
549             throw new MojoFailureException(message);
550         }
551 
552         if (addOptions != null && !addOptions.isEmpty()) {
553             requireJdk14();
554         }
555     }
556 
557     private void requireJdk14() throws MojoFailureException {
558         // needs JDK 14+
559         Optional<Toolchain> optToolchain = getToolchain();
560         String java14reqMsg = "parameter 'addOptions' needs at least a Java 14 runtime or a Java 14 toolchain.";
561 
562         if (optToolchain.isPresent()) {
563             Toolchain toolchain = optToolchain.orElseThrow(NoSuchElementException::new);
564             if (!(toolchain instanceof ToolchainPrivate)) {
565                 getLog().warn("Unable to check toolchain java version.");
566                 return;
567             }
568             ToolchainPrivate toolchainPrivate = (ToolchainPrivate) toolchain;
569             if (!toolchainPrivate.matchesRequirements(singletonMap("jdk", "14"))) {
570                 throw new MojoFailureException(java14reqMsg);
571             }
572         } else if (!JavaVersion.JAVA_VERSION.isAtLeast("14")) {
573             throw new MojoFailureException(java14reqMsg);
574         }
575     }
576 
577     /**
578      * Use a separate directory for each image.
579      *
580      * <p>Rationale: If a user creates multiple jlink artifacts using classifiers,
581      * the directories should not overwrite themselves for each execution.</p>
582      */
583     private void setOutputDirectoryImage() {
584         if (hasClassifier()) {
585             final File classifiersDirectory = new File(outputDirectoryImage, "classifiers");
586             outputDirectoryImage = new File(classifiersDirectory, classifier);
587         } else {
588             outputDirectoryImage = new File(outputDirectoryImage, "default");
589         }
590     }
591 
592     private void ifOutputDirectoryExistsDelteIt() throws MojoExecutionException {
593         if (outputDirectoryImage.exists()) {
594             // Delete the output folder of JLink before we start
595             // otherwise JLink will fail with a message "Error: directory already exists: ..."
596             try {
597                 getLog().debug("Deleting existing " + outputDirectoryImage.getAbsolutePath());
598                 FileUtils.forceDelete(outputDirectoryImage);
599             } catch (IOException e) {
600                 getLog().error("IOException", e);
601                 throw new MojoExecutionException(
602                         "Failure during deletion of " + outputDirectoryImage.getAbsolutePath() + " occured.");
603             }
604         }
605     }
606 
607     protected List<String> createJlinkArgs(Collection<String> pathsOfModules, Collection<String> modulesToAdd) {
608         List<String> jlinkArgs = new ArrayList<>();
609 
610         if (stripDebug) {
611             jlinkArgs.add("--strip-debug");
612         }
613 
614         if (bindServices) {
615             jlinkArgs.add("--bind-services");
616         }
617 
618         if (endian != null) {
619             jlinkArgs.add("--endian");
620             jlinkArgs.add(endian);
621         }
622         if (ignoreSigningInformation) {
623             jlinkArgs.add("--ignore-signing-information");
624         }
625         if (compress != null) {
626             jlinkArgs.add("--compress");
627             jlinkArgs.add(compress);
628         }
629         if (launcher != null) {
630             jlinkArgs.add("--launcher");
631             jlinkArgs.add(launcher);
632         }
633         if (addOptions != null && !addOptions.isEmpty()) {
634             jlinkArgs.add("--add-options");
635             jlinkArgs.add(String.format("\"%s\"", String.join(" ", addOptions)));
636         }
637 
638         if (disablePlugin != null) {
639             jlinkArgs.add("--disable-plugin");
640             jlinkArgs.add(disablePlugin);
641         }
642         if (pathsOfModules != null && !pathsOfModules.isEmpty()) {
643             // @formatter:off
644             jlinkArgs.add("--module-path");
645             jlinkArgs.add(getPlatformDependSeparateList(pathsOfModules).replace("\\", "\\\\"));
646             // @formatter:off
647         }
648 
649         if (noHeaderFiles) {
650             jlinkArgs.add("--no-header-files");
651         }
652 
653         if (noManPages) {
654             jlinkArgs.add("--no-man-pages");
655         }
656 
657         if (hasSuggestProviders()) {
658             jlinkArgs.add("--suggest-providers");
659             String sb = getCommaSeparatedList(suggestProviders);
660             jlinkArgs.add(sb);
661         }
662 
663         if (hasLimitModules()) {
664             jlinkArgs.add("--limit-modules");
665             String sb = getCommaSeparatedList(limitModules);
666             jlinkArgs.add(sb);
667         }
668 
669         if (!modulesToAdd.isEmpty()) {
670             jlinkArgs.add("--add-modules");
671             // This must be name of the module and *NOT* the name of the
672             // file! Can we somehow pre check this information to fail early?
673             String sb = getCommaSeparatedList(modulesToAdd);
674             jlinkArgs.add(sb.replace("\\", "\\\\"));
675         }
676 
677         if (hasIncludeLocales()) {
678             jlinkArgs.add("--add-modules");
679             jlinkArgs.add("jdk.localedata");
680             jlinkArgs.add("--include-locales");
681             String sb = getCommaSeparatedList(includeLocales);
682             jlinkArgs.add(sb);
683         }
684 
685         if (pluginModulePath != null) {
686             jlinkArgs.add("--plugin-module-path");
687             StringBuilder sb = convertSeparatedModulePathToPlatformSeparatedModulePath(pluginModulePath);
688             jlinkArgs.add(sb.toString().replace("\\", "\\\\"));
689         }
690 
691         if (buildDirectory != null) {
692             jlinkArgs.add("--output");
693             jlinkArgs.add(outputDirectoryImage.getAbsolutePath());
694         }
695 
696         if (verbose) {
697             jlinkArgs.add("--verbose");
698         }
699 
700         return Collections.unmodifiableList(jlinkArgs);
701     }
702 
703     private boolean hasIncludeLocales() {
704         return includeLocales != null && !includeLocales.isEmpty();
705     }
706 
707     private boolean hasSuggestProviders() {
708         return suggestProviders != null && !suggestProviders.isEmpty();
709     }
710 
711     private boolean hasLimitModules() {
712         return limitModules != null && !limitModules.isEmpty();
713     }
714 
715     /**
716      * {@inheritDoc}
717      */
718     protected String getClassifier() {
719         return classifier;
720     }
721 }