1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.plugins.jlink;
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40 import javax.inject.Inject;
41
42 import java.io.File;
43 import java.io.IOException;
44 import java.nio.file.attribute.FileTime;
45 import java.time.Instant;
46 import java.util.ArrayList;
47 import java.util.Collection;
48 import java.util.Collections;
49 import java.util.HashMap;
50 import java.util.List;
51 import java.util.Map;
52 import java.util.Map.Entry;
53 import java.util.NoSuchElementException;
54 import java.util.Optional;
55
56 import org.apache.commons.io.FileUtils;
57 import org.apache.maven.archiver.MavenArchiver;
58 import org.apache.maven.artifact.Artifact;
59 import org.apache.maven.model.Resource;
60 import org.apache.maven.plugin.MojoExecutionException;
61 import org.apache.maven.plugin.MojoFailureException;
62 import org.apache.maven.plugins.annotations.LifecyclePhase;
63 import org.apache.maven.plugins.annotations.Mojo;
64 import org.apache.maven.plugins.annotations.Parameter;
65 import org.apache.maven.plugins.annotations.ResolutionScope;
66 import org.apache.maven.project.MavenProject;
67 import org.apache.maven.project.MavenProjectHelper;
68 import org.apache.maven.shared.filtering.MavenFilteringException;
69 import org.apache.maven.shared.filtering.MavenResourcesExecution;
70 import org.apache.maven.shared.filtering.MavenResourcesFiltering;
71 import org.apache.maven.toolchain.Toolchain;
72 import org.apache.maven.toolchain.ToolchainManager;
73 import org.apache.maven.toolchain.ToolchainPrivate;
74 import org.apache.maven.toolchain.java.JavaToolchainImpl;
75 import org.codehaus.plexus.archiver.ArchiverException;
76 import org.codehaus.plexus.archiver.zip.ZipArchiver;
77 import org.codehaus.plexus.languages.java.jpms.JavaModuleDescriptor;
78 import org.codehaus.plexus.languages.java.jpms.LocationManager;
79 import org.codehaus.plexus.languages.java.jpms.ResolvePathsRequest;
80 import org.codehaus.plexus.languages.java.jpms.ResolvePathsResult;
81 import org.codehaus.plexus.languages.java.version.JavaVersion;
82
83 import static java.util.Collections.singletonMap;
84
85
86
87
88
89
90
91
92 @Mojo(name = "jlink", requiresDependencyResolution = ResolutionScope.RUNTIME, defaultPhase = LifecyclePhase.PACKAGE)
93 public class JLinkMojo extends AbstractJLinkMojo {
94
95
96
97
98
99
100
101
102 @Parameter
103 private Map<String, String> jdkToolchain;
104
105
106
107
108
109 @Parameter(defaultValue = "false")
110 private boolean stripDebug;
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131 @Parameter
132 private String compress;
133
134
135
136
137
138
139 @Parameter
140 private String launcher;
141
142
143
144
145
146
147
148
149
150 @Parameter
151 private List<String> launchers;
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171 @Parameter
172 private List<String> addOptions;
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190 @Parameter
191 private List<String> limitModules;
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213 @Parameter
214 private List<String> addModules;
215
216
217
218
219
220 @Parameter
221 private String pluginModulePath;
222
223
224
225
226
227
228
229
230
231 @Parameter(defaultValue = "${project.build.directory}/maven-jlink", required = true, readonly = true)
232 private File outputDirectoryImage;
233
234 @Parameter(defaultValue = "${project.build.directory}", required = true, readonly = true)
235 private File buildDirectory;
236
237 @Parameter(defaultValue = "${project.build.outputDirectory}", required = true, readonly = true)
238 private File outputDirectory;
239
240
241
242
243
244
245 @Parameter
246 private String endian;
247
248
249
250
251
252 @Parameter
253 private List<String> modulePaths;
254
255
256
257
258 @Parameter(defaultValue = "false")
259 private boolean bindServices;
260
261
262
263
264 @Parameter
265 private String disablePlugin;
266
267
268
269
270 @Parameter(defaultValue = "false")
271 private boolean ignoreSigningInformation;
272
273
274
275
276
277 @Parameter(defaultValue = "false")
278 private boolean noHeaderFiles;
279
280
281
282
283
284 @Parameter(defaultValue = "false")
285 private boolean noManPages;
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301 @Parameter
302 private List<String> suggestProviders;
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322 @Parameter
323 private List<String> includeLocales;
324
325
326
327
328 @Parameter(defaultValue = "false")
329 private boolean verbose;
330
331
332
333
334 @Parameter
335 private File sourceJdkModules;
336
337
338
339
340
341
342 @Parameter(defaultValue = "true")
343 private boolean attach;
344
345
346
347
348
349
350
351 @Parameter
352 private String classifier;
353
354
355
356
357
358 @Parameter(defaultValue = "${project.build.finalName}", readonly = true)
359 private String finalName;
360
361
362
363
364
365
366
367
368 @Parameter(defaultValue = "${project.build.outputTimestamp}")
369 private String outputTimestamp;
370
371
372
373
374
375
376 @Parameter
377 private List<Resource> additionalResources;
378
379
380
381
382
383
384
385
386
387
388
389 @Parameter(defaultValue = "")
390 private String zipDirPrefix;
391
392
393
394
395 private final MavenProjectHelper projectHelper;
396
397 private final MavenResourcesFiltering mavenResourcesFiltering;
398
399 private final LocationManager locationManager;
400
401
402
403
404 private final ZipArchiver zipArchiver = new ZipArchiver();
405
406 @Inject
407 public JLinkMojo(
408 MavenProjectHelper projectHelper,
409 ToolchainManager toolchainManager,
410 MavenResourcesFiltering mavenResourcesFiltering,
411 LocationManager locationManager) {
412 super(toolchainManager);
413 this.mavenResourcesFiltering = mavenResourcesFiltering;
414 this.projectHelper = projectHelper;
415 this.locationManager = locationManager;
416 }
417
418 @Override
419 public void execute() throws MojoExecutionException, MojoFailureException {
420 failIfParametersAreNotInTheirValidValueRanges();
421
422 setOutputDirectoryImage();
423
424 ifOutputDirectoryExistsDelteIt();
425
426 JLinkExecutor jLinkExec = getJlinkExecutor();
427 Collection<String> modulesToAdd = new ArrayList<>();
428 if (addModules != null) {
429 modulesToAdd.addAll(addModules);
430 }
431 jLinkExec.addAllModules(modulesToAdd);
432
433 Collection<String> pathsOfModules = new ArrayList<>();
434 if (modulePaths != null) {
435 pathsOfModules.addAll(modulePaths);
436 }
437
438 for (Entry<String, File> item : getModulePathElements().entrySet()) {
439 getLog().info(" -> module: " + item.getKey() + " ( "
440 + item.getValue().getPath() + " )");
441
442
443 modulesToAdd.add(item.getKey());
444 pathsOfModules.add(item.getValue().getPath());
445 }
446
447
448 jLinkExec
449 .getJmodsFolder(this.sourceJdkModules)
450 .ifPresent(jmodsFolder -> pathsOfModules.add(jmodsFolder.getAbsolutePath()));
451 jLinkExec.addAllModulePaths(pathsOfModules);
452
453 List<String> jlinkArgs = createJlinkArgs(pathsOfModules, modulesToAdd);
454
455 try {
456 jLinkExec.executeJlink(jlinkArgs);
457 } catch (IllegalStateException e) {
458 throw new MojoFailureException("Unable to find jlink command: " + e.getMessage(), e);
459 }
460
461
462 try {
463 mavenResourcesFiltering.filterResources(new MavenResourcesExecution(
464 additionalResources,
465 outputDirectoryImage,
466 getProject(),
467 "UTF-8",
468 Collections.emptyList(),
469 Collections.emptyList(),
470 getSession()));
471 } catch (MavenFilteringException e) {
472 throw new MojoFailureException("Unable to copy the additional resources: " + e.getMessage(), e);
473 }
474
475 File createZipArchiveFromImage = createZipArchiveFromImage(buildDirectory, outputDirectoryImage);
476
477 attachArtifactUnlessDisabled(createZipArchiveFromImage);
478 }
479
480
481
482
483
484
485
486 List<File> getCompileClasspathElements(MavenProject project) {
487 List<File> list = new ArrayList<>(project.getArtifacts().size() + 1);
488
489 for (Artifact artifact : project.getArtifacts()) {
490 boolean shouldSkip = shouldSkip(artifact);
491 getLog().debug("Adding artifact: " + artifact.getGroupId() + ":" + artifact.getArtifactId() + ":"
492 + artifact.getVersion() + (shouldSkip ? " (skipping)" : ""));
493 if (!shouldSkip) {
494 list.add(artifact.getFile());
495 }
496 }
497 return list;
498 }
499
500
501
502
503
504
505
506
507 private boolean shouldSkip(Artifact artifact) {
508 return "pom".equals(artifact.getType());
509 }
510
511 Map<String, File> getModulePathElements() throws MojoFailureException {
512
513
514
515
516 Map<String, File> modulepathElements = new HashMap<>();
517
518 try {
519 Collection<File> dependencyArtifacts = getCompileClasspathElements(getProject());
520
521 ResolvePathsRequest<File> request = ResolvePathsRequest.ofFiles(dependencyArtifacts);
522
523 Optional<Toolchain> toolchain = getToolchain();
524 if (toolchain.isPresent()
525 && toolchain.orElseThrow(NoSuchElementException::new) instanceof JavaToolchainImpl) {
526 Toolchain toolchain1 = toolchain.orElseThrow(NoSuchElementException::new);
527 request.setJdkHome(new File(((JavaToolchainImpl) toolchain1).getJavaHome()));
528 }
529
530 ResolvePathsResult<File> resolvePathsResult = locationManager.resolvePaths(request);
531
532 for (Map.Entry<File, JavaModuleDescriptor> entry :
533 resolvePathsResult.getPathElements().entrySet()) {
534 JavaModuleDescriptor descriptor = entry.getValue();
535 if (descriptor == null) {
536 String message = "The given dependency " + entry.getKey()
537 + " does not have a module-info.java file. So it can't be linked.";
538 getLog().error(message);
539 throw new MojoFailureException(message);
540 }
541
542
543 if (descriptor.isAutomatic()) {
544 getLog().debug("Ignoring automatic module: " + descriptor.name());
545 continue;
546 }
547
548 if (modulepathElements.containsKey(descriptor.name())) {
549 getLog().warn("The module name " + descriptor.name() + " does already exists.");
550 }
551 modulepathElements.put(descriptor.name(), entry.getKey());
552 }
553
554
555
556 if (outputDirectory.exists()) {
557 List<File> singletonList = Collections.singletonList(outputDirectory);
558
559 ResolvePathsRequest<File> singleModuls = ResolvePathsRequest.ofFiles(singletonList);
560
561 ResolvePathsResult<File> resolvePaths = locationManager.resolvePaths(singleModuls);
562 for (Entry<File, JavaModuleDescriptor> entry :
563 resolvePaths.getPathElements().entrySet()) {
564 JavaModuleDescriptor descriptor = entry.getValue();
565 if (descriptor == null) {
566 String message = "The given project " + entry.getKey()
567 + " does not contain a module-info.java file. So it can't be linked.";
568 getLog().error(message);
569 throw new MojoFailureException(message);
570 }
571
572 if (descriptor.isAutomatic()) {
573 getLog().debug("Ignoring automatic module: " + descriptor.name());
574 continue;
575 }
576
577 if (modulepathElements.containsKey(descriptor.name())) {
578 getLog().warn("The module name " + descriptor.name() + " does already exists.");
579 }
580 modulepathElements.put(descriptor.name(), entry.getKey());
581 }
582 }
583
584 } catch (IOException e) {
585 getLog().error(e.getMessage());
586 throw new MojoFailureException(e.getMessage());
587 }
588
589 return modulepathElements;
590 }
591
592 private boolean projectHasAlreadySetAnArtifact() {
593 if (getProject().getArtifact().getFile() != null) {
594 return getProject().getArtifact().getFile().isFile();
595 } else {
596 return false;
597 }
598 }
599
600
601
602
603 private boolean hasClassifier() {
604 return hasClassifier(getClassifier());
605 }
606
607 private File createZipArchiveFromImage(File outputDirectory, File outputDirectoryImage)
608 throws MojoExecutionException {
609 if (zipDirPrefix != null && !zipDirPrefix.isEmpty() && !zipDirPrefix.endsWith("/")) {
610 zipDirPrefix += "/";
611 }
612 zipArchiver.addDirectory(outputDirectoryImage, zipDirPrefix);
613
614
615 Optional<Instant> lastModified = MavenArchiver.parseBuildOutputTimestamp(outputTimestamp);
616 if (lastModified.isPresent()) {
617 zipArchiver.configureReproducibleBuild(FileTime.from(lastModified.get()));
618 }
619
620 File resultArchive = getZipFile(outputDirectory, finalName, getClassifier());
621
622 zipArchiver.setDestFile(resultArchive);
623 try {
624 zipArchiver.createArchive();
625 } catch (ArchiverException | IOException e) {
626 getLog().error(e.getMessage(), e);
627 throw new MojoExecutionException(e.getMessage(), e);
628 }
629
630 return resultArchive;
631 }
632
633 private void attachArtifactUnlessDisabled(File artifactFile) throws MojoExecutionException {
634 if (!attach) {
635 return;
636 }
637
638 if (hasClassifier()) {
639 projectHelper.attachArtifact(getProject(), "jlink", getClassifier(), artifactFile);
640 } else {
641 if (projectHasAlreadySetAnArtifact()) {
642 throw new MojoExecutionException("You have to use a classifier "
643 + "to attach supplemental artifacts to the project instead of replacing them.");
644 }
645 getProject().getArtifact().setFile(artifactFile);
646 }
647 }
648
649 private void failIfParametersAreNotInTheirValidValueRanges() throws MojoFailureException {
650 if (endian != null && (!"big".equals(endian) && !"little".equals(endian))) {
651 String message = "The given endian parameter " + endian
652 + " does not contain one of the following values: 'little' or 'big'.";
653 getLog().error(message);
654 throw new MojoFailureException(message);
655 }
656
657 if (addOptions != null && !addOptions.isEmpty()) {
658 requireJdk14();
659 }
660 }
661
662 private void requireJdk14() throws MojoFailureException {
663
664 Optional<Toolchain> optToolchain = getToolchain();
665 String java14reqMsg = "parameter 'addOptions' needs at least a Java 14 runtime or a Java 14 toolchain.";
666
667 if (optToolchain.isPresent()) {
668 Toolchain toolchain = optToolchain.orElseThrow(NoSuchElementException::new);
669 if (!(toolchain instanceof ToolchainPrivate)) {
670 getLog().warn("Unable to check toolchain java version.");
671 return;
672 }
673 ToolchainPrivate toolchainPrivate = (ToolchainPrivate) toolchain;
674 if (!toolchainPrivate.matchesRequirements(singletonMap("jdk", "14"))) {
675 throw new MojoFailureException(java14reqMsg);
676 }
677 } else if (!JavaVersion.JAVA_VERSION.isAtLeast("14")) {
678 throw new MojoFailureException(java14reqMsg);
679 }
680 }
681
682
683
684
685
686
687
688 private void setOutputDirectoryImage() {
689 if (hasClassifier()) {
690 final File classifiersDirectory = new File(outputDirectoryImage, "classifiers");
691 outputDirectoryImage = new File(classifiersDirectory, classifier);
692 } else {
693 outputDirectoryImage = new File(outputDirectoryImage, "default");
694 }
695 }
696
697 private void ifOutputDirectoryExistsDelteIt() throws MojoExecutionException {
698 if (outputDirectoryImage.exists()) {
699
700
701 try {
702 getLog().debug("Deleting existing " + outputDirectoryImage.getAbsolutePath());
703 FileUtils.forceDelete(outputDirectoryImage);
704 } catch (IOException e) {
705 getLog().error("IOException", e);
706 throw new MojoExecutionException(
707 "Failure during deletion of " + outputDirectoryImage.getAbsolutePath() + " occured.");
708 }
709 }
710 }
711
712 protected List<String> createJlinkArgs(Collection<String> pathsOfModules, Collection<String> modulesToAdd)
713 throws MojoExecutionException {
714 List<String> jlinkArgs = new ArrayList<>();
715
716 if (stripDebug) {
717 jlinkArgs.add("--strip-debug");
718 }
719
720 if (bindServices) {
721 jlinkArgs.add("--bind-services");
722 }
723
724 if (endian != null) {
725 jlinkArgs.add("--endian");
726 jlinkArgs.add(endian);
727 }
728 if (ignoreSigningInformation) {
729 jlinkArgs.add("--ignore-signing-information");
730 }
731 if (compress != null) {
732 jlinkArgs.add("--compress");
733 jlinkArgs.add(compress);
734 }
735 if (launcher != null) {
736 if (launchers != null) {
737 throw new MojoExecutionException("Specify either single <launcher> or multiple <launchers>, not both.");
738 } else {
739 launchers = List.of(launcher);
740 }
741 }
742 if (launchers != null) {
743 for (String item : launchers) {
744 jlinkArgs.add("--launcher");
745 jlinkArgs.add(item);
746 }
747 }
748 if (addOptions != null && !addOptions.isEmpty()) {
749 jlinkArgs.add("--add-options=" + String.join(" ", addOptions));
750 }
751
752 if (disablePlugin != null) {
753 jlinkArgs.add("--disable-plugin");
754 jlinkArgs.add(disablePlugin);
755 }
756 if (pathsOfModules != null && !pathsOfModules.isEmpty()) {
757
758 jlinkArgs.add("--module-path");
759 jlinkArgs.add(String.join(File.pathSeparator, pathsOfModules).replace("\\", "\\\\"));
760
761 }
762
763 if (noHeaderFiles) {
764 jlinkArgs.add("--no-header-files");
765 }
766
767 if (noManPages) {
768 jlinkArgs.add("--no-man-pages");
769 }
770
771 if (hasLimitModules()) {
772 jlinkArgs.add("--limit-modules");
773 String sb = String.join(",", limitModules);
774 jlinkArgs.add(sb);
775 }
776
777 if (!modulesToAdd.isEmpty()) {
778 jlinkArgs.add("--add-modules");
779
780
781 String sb = String.join(",", modulesToAdd);
782 jlinkArgs.add(sb.replace("\\", "\\\\"));
783 }
784
785 if (hasIncludeLocales()) {
786 jlinkArgs.add("--add-modules");
787 jlinkArgs.add("jdk.localedata");
788 jlinkArgs.add("--include-locales");
789 String sb = String.join(",", includeLocales);
790 jlinkArgs.add(sb);
791 }
792
793 if (pluginModulePath != null) {
794 jlinkArgs.add("--plugin-module-path");
795 StringBuilder sb = convertSeparatedModulePathToPlatformSeparatedModulePath(pluginModulePath);
796 jlinkArgs.add(sb.toString().replace("\\", "\\\\"));
797 }
798
799 if (buildDirectory != null) {
800 jlinkArgs.add("--output");
801 jlinkArgs.add(outputDirectoryImage.getAbsolutePath());
802 }
803
804 if (verbose) {
805 jlinkArgs.add("--verbose");
806 }
807
808
809 if (hasSuggestProviders()) {
810 jlinkArgs.add("--suggest-providers");
811 String sb = String.join(",", suggestProviders);
812 jlinkArgs.add(sb);
813 }
814
815 return Collections.unmodifiableList(jlinkArgs);
816 }
817
818 private boolean hasIncludeLocales() {
819 return includeLocales != null && !includeLocales.isEmpty();
820 }
821
822 private boolean hasSuggestProviders() {
823 return suggestProviders != null && !suggestProviders.isEmpty();
824 }
825
826 private boolean hasLimitModules() {
827 return limitModules != null && !limitModules.isEmpty();
828 }
829
830
831
832
833 @Override
834 protected String getClassifier() {
835 return classifier;
836 }
837
838
839
840
841
842
843
844
845
846 private static File getZipFile(File basedir, String finalName, String classifier) {
847 if (finalName.isEmpty()) {
848 throw new IllegalArgumentException("finalName is not allowed to be empty.");
849 }
850
851 StringBuilder fileName = new StringBuilder(finalName);
852
853 if (hasClassifier(classifier)) {
854 fileName.append("-").append(classifier);
855 }
856
857 fileName.append(".zip");
858
859 return new File(basedir, fileName.toString());
860 }
861
862 private static boolean hasClassifier(String classifier) {
863 boolean result = false;
864 if (classifier != null && !classifier.isEmpty()) {
865 result = true;
866 }
867
868 return result;
869 }
870 }