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.LinkedHashMap;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Map.Entry;
31  
32  import org.apache.maven.artifact.Artifact;
33  import org.apache.maven.plugin.MojoExecutionException;
34  import org.apache.maven.plugin.MojoFailureException;
35  import org.apache.maven.plugins.annotations.Component;
36  import org.apache.maven.plugins.annotations.LifecyclePhase;
37  import org.apache.maven.plugins.annotations.Mojo;
38  import org.apache.maven.plugins.annotations.Parameter;
39  import org.apache.maven.plugins.annotations.ResolutionScope;
40  import org.apache.maven.project.MavenProject;
41  import org.apache.maven.shared.utils.StringUtils;
42  import org.apache.maven.shared.utils.logging.MessageUtils;
43  import org.apache.maven.toolchain.Toolchain;
44  import org.apache.maven.toolchain.java.DefaultJavaToolChain;
45  import org.codehaus.plexus.archiver.Archiver;
46  import org.codehaus.plexus.archiver.ArchiverException;
47  import org.codehaus.plexus.archiver.zip.ZipArchiver;
48  import org.codehaus.plexus.languages.java.jpms.JavaModuleDescriptor;
49  import org.codehaus.plexus.languages.java.jpms.LocationManager;
50  import org.codehaus.plexus.languages.java.jpms.ResolvePathsRequest;
51  import org.codehaus.plexus.languages.java.jpms.ResolvePathsResult;
52  import org.codehaus.plexus.languages.java.jpms.ResolvePathsResult.ModuleNameSource;
53  import org.codehaus.plexus.util.FileUtils;
54  import org.codehaus.plexus.util.cli.Commandline;
55  
56  /**
57   * The JLink goal is intended to create a Java Run Time Image file based on
58   * <a href="http://openjdk.java.net/jeps/282">http://openjdk.java.net/jeps/282</a>,
59   * <a href="http://openjdk.java.net/jeps/220">http://openjdk.java.net/jeps/220</a>.
60   * 
61   * @author Karl Heinz Marbaise <a href="mailto:khmarbaise@apache.org">khmarbaise@apache.org</a>
62   */
63  // CHECKSTYLE_OFF: LineLength
64  @Mojo( name = "jlink", requiresDependencyCollection = ResolutionScope.RUNTIME, defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true )
65  // CHECKSTYLE_ON: LineLength
66  public class JLinkMojo
67      extends AbstractJLinkMojo
68  {
69      private static final String JMODS = "jmods";
70  
71      private List<String> classpathElements;
72  
73      private List<String> modulepathElements;
74  
75      private Map<String, JavaModuleDescriptor> pathElements;
76  
77      @Component
78      private LocationManager locationManager;
79  
80      /**
81       * <p>
82       * Specify the requirements for this jdk toolchain. This overrules the toolchain selected by the
83       * maven-toolchain-plugin.
84       * </p>
85       * <strong>note:</strong> requires at least Maven 3.3.1
86       */
87      @Parameter
88      private Map<String, String> jdkToolchain;
89  
90      /**
91       * This is intended to strip debug information out. The command line equivalent of <code>jlink</code> is:
92       * <code>-G, --strip-debug</code> strip debug information.
93       */
94      @Parameter( defaultValue = "false" )
95      private boolean stripDebug;
96  
97      /**
98       * Here you can define the compression of the resources being used. The command line equivalent is:
99       * <code>-c, --compress=level&gt;</code>. The valid values for the level are: <code>0, 1, 2</code>.
100      */
101     @Parameter
102     private Integer compression;
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 outputDirectory;
163 
164     /**
165      * The byte order of the generated Java Run Time image. <code>--endian &lt;little|big&gt;</code>. If the endian is
166      * not given the default is: <code>native</code>.
167      */
168     // TODO: Should we define either little or big as default? or should we left as it.
169     @Parameter
170     private String endian;
171 
172     private List<String> modulePaths;
173 
174     /**
175      * Add the option <code>--bind-services</code> or not.
176      */
177     @Parameter( defaultValue = "false" )
178     private boolean bindServices;
179 
180     /**
181      * You can disable a plugin by using this option. <code>--disable-plugin pluginName</code>.
182      */
183     @Parameter
184     private String disablePlugin;
185 
186     /**
187      * <code>--ignore-signing-information</code>
188      */
189     @Parameter( defaultValue = "false" )
190     private boolean ignoreSigningInformation;
191 
192     /**
193      * This will suppress to have an <code>includes</code> directory in the resulting Java Run Time Image. The JLink
194      * command line equivalent is: <code>--no-header-files</code>
195      */
196     @Parameter( defaultValue = "false" )
197     private boolean noHeaderFiles;
198 
199     /**
200      * This will suppress to have the <code>man</code> directory in the resulting Java Run Time Image. The JLink command
201      * line equivalent is: <code>--no-man-pages</code>
202      */
203     @Parameter( defaultValue = "false" )
204     private boolean noManPages;
205 
206     /**
207      * Suggest providers that implement the given service types from the module path.
208      * 
209      * <pre>
210      * &lt;suggestProviders&gt;
211      *   &lt;suggestProvider&gt;name-a&lt;/suggestProvider&gt;
212      *   &lt;suggestProvider&gt;name-b&lt;/suggestProvider&gt;
213      *   .
214      *   .
215      * &lt;/suggestProviders&gt;
216      * </pre>
217      * 
218      * The jlink command linke equivalent: <code>--suggest-providers [&lt;name&gt;,...]</code>
219      */
220     @Parameter
221     private List<String> suggestProviders;
222 
223     /**
224      * This will turn on verbose mode. The jlink command line equivalent is: <code>--verbose</code>
225      */
226     @Parameter( defaultValue = "false" )
227     private boolean verbose;
228 
229     /**
230      * The JAR archiver needed for archiving the environments.
231      */
232     @Component( role = Archiver.class, hint = "zip" )
233     private ZipArchiver zipArchiver;
234 
235     /**
236      * Name of the generated ZIP file in the <code>target</code> directory. 
237      * This will not change the name of the installed/deployed file.
238      */
239     @Parameter( defaultValue = "${project.build.finalName}", readonly = true )
240     private String finalName;
241 
242     public void execute()
243         throws MojoExecutionException, MojoFailureException
244     {
245 
246         String jLinkExec = getExecutable();
247 
248         getLog().info( "Toolchain in maven-jlink-plugin: jlink [ " + jLinkExec + " ]" );
249 
250         // TODO: Find a more better and cleaner way?
251         File jLinkExecuteable = new File( jLinkExec );
252 
253         // Really Hacky...do we have a better solution to find the jmods directory of the JDK?
254         File jLinkParent = jLinkExecuteable.getParentFile().getParentFile();
255         File jmodsFolder = new File( jLinkParent, JMODS );
256 
257         getLog().debug( " Parent: " + jLinkParent.getAbsolutePath() );
258         getLog().debug( " jmodsFolder: " + jmodsFolder.getAbsolutePath() );
259 
260         failIfParametersAreNotInTheirValidValueRanges();
261 
262         ifOutputDirectoryExistsDelteIt();
263 
264         preparePaths();
265 
266         getLog().info( "The following dependencies will be linked into the runtime image:" );
267 
268         this.addModules = new ArrayList<>();
269         this.modulePaths = new ArrayList<>();
270         for ( Entry<String, JavaModuleDescriptor> item : pathElements.entrySet() )
271         {
272             // Isn't there a better solution?
273             if ( item.getValue() == null )
274             {
275                 String message = "The given dependency " + item.getKey()
276                     + " does not have a module-info.java file. So it can't be linked.";
277                 getLog().error( message );
278                 throw new MojoFailureException( message );
279             }
280             getLog().debug( "pathElements Item:" + item.getKey() + " v:" + item.getValue().name() );
281             getLog().info( " -> module: " + item.getValue().name() + " ( " + item.getKey() + " )" );
282             // We use the real module name and not the artifact Id...
283             this.addModules.add( item.getValue().name() );
284             this.modulePaths.add( item.getKey() );
285         }
286         // The jmods directory of the JDK
287         this.modulePaths.add( jmodsFolder.getAbsolutePath() );
288 
289         Commandline cmd;
290         try
291         {
292             cmd = createJLinkCommandLine();
293         }
294         catch ( IOException e )
295         {
296             throw new MojoExecutionException( e.getMessage() );
297         }
298         cmd.setExecutable( jLinkExec );
299 
300         executeCommand( cmd, outputDirectoryImage );
301 
302         File createZipArchiveFromImage = createZipArchiveFromImage( outputDirectory, outputDirectoryImage );
303 
304         if ( projectHasAlreadySetAnArtifact() )
305         {
306             throw new MojoExecutionException( "You have to use a classifier "
307                 + "to attach supplemental artifacts to the project instead of replacing them." );
308         }
309 
310         getProject().getArtifact().setFile( createZipArchiveFromImage );
311     }
312 
313     private List<File> getCompileClasspathElements( MavenProject project )
314     {
315         List<File> list = new ArrayList<File>( project.getArtifacts().size() + 1 );
316 
317         for ( Artifact a : project.getArtifacts() )
318         {
319             list.add( a.getFile() );
320         }
321         return list;
322     }
323 
324     private void preparePaths()
325     {
326         // For now only allow named modules. Once we can create a graph with ASM we can specify exactly the modules
327         // and we can detect if auto modules are used. In that case, MavenProject.setFile() should not be used, so
328         // you cannot depend on this project and so it won't be distributed.
329 
330         modulepathElements = new ArrayList<String>();
331         classpathElements = new ArrayList<String>();
332         pathElements = new LinkedHashMap<String, JavaModuleDescriptor>();
333 
334         ResolvePathsResult<File> resolvePathsResult;
335         try
336         {
337             Collection<File> dependencyArtifacts = getCompileClasspathElements( getProject() );
338 
339             ResolvePathsRequest<File> request = ResolvePathsRequest.withFiles( dependencyArtifacts );
340 
341             Toolchain toolchain = getToolchain();
342             if ( toolchain != null && toolchain instanceof DefaultJavaToolChain )
343             {
344                 request.setJdkHome( new File( ( (DefaultJavaToolChain) toolchain ).getJavaHome() ) );
345             }
346 
347             resolvePathsResult = locationManager.resolvePaths( request );
348 
349             JavaModuleDescriptor moduleDescriptor = resolvePathsResult.getMainModuleDescriptor();
350 
351             for ( Map.Entry<File, ModuleNameSource> entry : resolvePathsResult.getModulepathElements().entrySet() )
352             {
353                 if ( ModuleNameSource.FILENAME.equals( entry.getValue() ) )
354                 {
355                     final String message = "Required filename-based automodules detected. "
356                         + "Please don't publish this project to a public artifact repository!";
357 
358                     if ( moduleDescriptor.exports().isEmpty() )
359                     {
360                         // application
361                         getLog().info( message );
362                     }
363                     else
364                     {
365                         // library
366                         writeBoxedWarning( message );
367                     }
368                     break;
369                 }
370             }
371 
372             for ( Map.Entry<File, JavaModuleDescriptor> entry : resolvePathsResult.getPathElements().entrySet() )
373             {
374                 pathElements.put( entry.getKey().getPath(), entry.getValue() );
375             }
376 
377             for ( File file : resolvePathsResult.getClasspathElements() )
378             {
379                 classpathElements.add( file.getPath() );
380             }
381 
382             for ( File file : resolvePathsResult.getModulepathElements().keySet() )
383             {
384                 modulepathElements.add( file.getPath() );
385             }
386         }
387         catch ( IOException e )
388         {
389             getLog().warn( e.getMessage() );
390         }
391     }
392 
393     private String getExecutable()
394         throws MojoFailureException
395     {
396         String jLinkExec;
397         try
398         {
399             jLinkExec = getJLinkExecutable();
400         }
401         catch ( IOException e )
402         {
403             throw new MojoFailureException( "Unable to find jlink command: " + e.getMessage(), e );
404         }
405         return jLinkExec;
406     }
407 
408     private boolean projectHasAlreadySetAnArtifact()
409     {
410         if ( getProject().getArtifact().getFile() != null )
411         {
412             return getProject().getArtifact().getFile().isFile();
413         }
414         else
415         {
416             return false;
417         }
418     }
419 
420     private File createZipArchiveFromImage( File outputDirectory, File outputDirectoryImage )
421         throws MojoExecutionException
422     {
423         zipArchiver.addDirectory( outputDirectoryImage );
424 
425         File resultArchive = getArchiveFile( outputDirectory, finalName, null, "zip" );
426 
427         zipArchiver.setDestFile( resultArchive );
428         try
429         {
430             zipArchiver.createArchive();
431         }
432         catch ( ArchiverException e )
433         {
434             getLog().error( e.getMessage(), e );
435             throw new MojoExecutionException( e.getMessage(), e );
436         }
437         catch ( IOException e )
438         {
439             getLog().error( e.getMessage(), e );
440             throw new MojoExecutionException( e.getMessage(), e );
441         }
442 
443         return resultArchive;
444 
445     }
446 
447     private void failIfParametersAreNotInTheirValidValueRanges()
448         throws MojoFailureException
449     {
450         if ( compression != null && ( compression < 0 || compression > 2 ) )
451         {
452             String message =
453                 "The given compression parameters " + compression + " is not in the valid value range from 0..2";
454             getLog().error( message );
455             throw new MojoFailureException( message );
456         }
457 
458         if ( endian != null && ( !"big".equals( endian ) || !"little".equals( endian ) ) )
459         {
460             String message = "The given endian parameter " + endian
461                 + " does not contain one of the following values: 'little' or 'big'.";
462             getLog().error( message );
463             throw new MojoFailureException( message );
464         }
465     }
466 
467     private void ifOutputDirectoryExistsDelteIt()
468         throws MojoExecutionException
469     {
470         if ( outputDirectoryImage.exists() )
471         {
472             // Delete the output folder of JLink before we start
473             // otherwise JLink will fail with a message "Error: directory already exists: ..."
474             try
475             {
476                 getLog().debug( "Deleting existing " + outputDirectoryImage.getAbsolutePath() );
477                 FileUtils.forceDelete( outputDirectoryImage );
478             }
479             catch ( IOException e )
480             {
481                 getLog().error( "IOException", e );
482                 throw new MojoExecutionException( "Failure during deletion of " + outputDirectoryImage.getAbsolutePath()
483                     + " occured." );
484             }
485         }
486     }
487 
488     private Commandline createJLinkCommandLine()
489         throws IOException
490     {
491         File file = new File( outputDirectoryImage.getParentFile(), "jlinkArgs" );
492         if ( !getLog().isDebugEnabled() )
493         {
494             file.deleteOnExit();
495         }
496         file.getParentFile().mkdirs();
497         file.createNewFile();
498 
499         PrintStream argsFile = new PrintStream( file );
500 
501         if ( stripDebug )
502         {
503             argsFile.println( "--strip-debug" );
504         }
505 
506         if ( bindServices )
507         {
508             argsFile.println( "--bind-services" );
509         }
510 
511         if ( endian != null )
512         {
513             argsFile.println( "--endians" );
514             argsFile.println( endian );
515         }
516         if ( ignoreSigningInformation )
517         {
518             argsFile.println( "--ignore-signing-information" );
519         }
520         if ( compression != null )
521         {
522             argsFile.println( "--compression" );
523             argsFile.println( compression );
524         }
525 
526         if ( disablePlugin != null )
527         {
528             argsFile.println( "--disable-plugin" );
529             argsFile.append( '"' ).append( disablePlugin ).println( '"' );
530 
531         }
532         if ( modulePaths != null )
533         {
534             //@formatter:off
535             argsFile.println( "--module-path" );
536             argsFile
537               .append( '"' )
538               .append( getPlatformDependSeparateList( modulePaths )
539                          .replace( "\\", "\\\\" ) 
540                      ).println( '"' );
541             //@formatter:off
542         }
543 
544         if ( noHeaderFiles )
545         {
546             argsFile.println( "--no-header-files" );
547         }
548 
549         if ( noManPages )
550         {
551             argsFile.println( "--no-man-pages" );
552         }
553 
554         if ( hasSuggestProviders() )
555         {
556             argsFile.println( "--suggest-providers" );
557             String sb = getCommaSeparatedList( suggestProviders );
558             argsFile.println( sb );
559         }
560 
561         if ( hasLimitModules() )
562         {
563             argsFile.println( "--limit-modules" );
564             String sb = getCommaSeparatedList( limitModules );
565             argsFile.println( sb );
566         }
567 
568         if ( hasModules() )
569         {
570             argsFile.println( "--add-modules" );
571             // This must be name of the module and *NOT* the name of the
572             // file! Can we somehow pre check this information to fail early?
573             String sb = getCommaSeparatedList( addModules );
574             argsFile.append( '"' ).append( sb.replace( "\\", "\\\\" ) ).println( '"' );
575         }
576 
577         if ( pluginModulePath != null )
578         {
579             argsFile.println( "--plugin-module-path" );
580             StringBuilder sb = convertSeparatedModulePathToPlatformSeparatedModulePath( pluginModulePath );
581             argsFile.append( '"' ).append( sb.toString().replace( "\\", "\\\\" ) ).println( '"' );
582         }
583 
584         if ( outputDirectory != null )
585         {
586             argsFile.println( "--output" );
587             argsFile.println( outputDirectoryImage );
588         }
589 
590         if ( verbose )
591         {
592             argsFile.println( "--verbose" );
593         }
594         argsFile.close();
595 
596         Commandline cmd = new Commandline();
597         cmd.createArg().setValue( '@' + file.getAbsolutePath() );
598 
599         return cmd;
600     }
601 
602     private boolean hasSuggestProviders()
603     {
604         return suggestProviders != null && !suggestProviders.isEmpty();
605     }
606 
607     private boolean hasLimitModules()
608     {
609         return limitModules != null && !limitModules.isEmpty();
610     }
611 
612     private boolean hasModules()
613     {
614         return addModules != null && !addModules.isEmpty();
615     }
616     
617     private void writeBoxedWarning( String message )
618     {
619         String line = StringUtils.repeat( "*", message.length() + 4 );
620         getLog().warn( line );
621         getLog().warn( "* " + MessageUtils.buffer().strong( message )  + " *" );
622         getLog().warn( line );
623     }
624     
625 }