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