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