1 /*
2  * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 package jdk.tools.jlink.builder;
27 
28 import java.io.BufferedOutputStream;
29 import java.io.BufferedWriter;
30 import java.io.ByteArrayInputStream;
31 import java.io.DataOutputStream;
32 import java.io.File;
33 import java.io.FileInputStream;
34 import java.io.FileOutputStream;
35 import java.io.IOException;
36 import java.io.InputStream;
37 import java.io.OutputStream;
38 import java.io.OutputStreamWriter;
39 import java.io.UncheckedIOException;
40 import java.io.Writer;
41 import java.lang.module.ModuleDescriptor;
42 import java.nio.charset.StandardCharsets;
43 import java.nio.file.FileAlreadyExistsException;
44 import java.nio.file.Files;
45 import java.nio.file.Path;
46 import java.nio.file.Paths;
47 import java.nio.file.StandardOpenOption;
48 import java.nio.file.attribute.PosixFilePermission;
49 import java.util.Collections;
50 import java.util.HashMap;
51 import java.util.HashSet;
52 import java.util.List;
53 import java.util.Map;
54 import java.util.Objects;
55 import java.util.Optional;
56 import java.util.Properties;
57 import java.util.Set;
58 import static java.util.stream.Collectors.*;
59 
60 import jdk.tools.jlink.internal.BasicImageWriter;
61 import jdk.tools.jlink.internal.ExecutableImage;
62 import jdk.tools.jlink.internal.Platform;
63 import jdk.tools.jlink.plugin.ResourcePool;
64 import jdk.tools.jlink.plugin.ResourcePoolEntry;
65 import jdk.tools.jlink.plugin.ResourcePoolEntry.Type;
66 import jdk.tools.jlink.plugin.ResourcePoolModule;
67 import jdk.tools.jlink.plugin.PluginException;
68 
69 /**
70  *
71  * Default Image Builder. This builder creates the default runtime image layout.
72  */
73 public final class DefaultImageBuilder implements ImageBuilder {
74     // Top-level directory names in a modular runtime image
75     public static final String BIN_DIRNAME      = "bin";
76     public static final String CONF_DIRNAME     = "conf";
77     public static final String INCLUDE_DIRNAME  = "include";
78     public static final String LIB_DIRNAME      = "lib";
79     public static final String LEGAL_DIRNAME    = "legal";
80     public static final String MAN_DIRNAME      = "man";
81 
82     /**
83      * The default java executable Image.
84      */
85     static final class DefaultExecutableImage implements ExecutableImage {
86 
87         private final Path home;
88         private final List<String> args;
89         private final Set<String> modules;
90 
DefaultExecutableImage(Path home, Set<String> modules)91         DefaultExecutableImage(Path home, Set<String> modules) {
92             Objects.requireNonNull(home);
93             if (!Files.exists(home)) {
94                 throw new IllegalArgumentException("Invalid image home");
95             }
96             this.home = home;
97             this.modules = Collections.unmodifiableSet(modules);
98             this.args = createArgs(home);
99         }
100 
createArgs(Path home)101         private static List<String> createArgs(Path home) {
102             Objects.requireNonNull(home);
103             Path binDir = home.resolve("bin");
104             String java = Files.exists(binDir.resolve("java"))? "java" : "java.exe";
105             return List.of(binDir.resolve(java).toString());
106         }
107 
108         @Override
getHome()109         public Path getHome() {
110             return home;
111         }
112 
113         @Override
getModules()114         public Set<String> getModules() {
115             return modules;
116         }
117 
118         @Override
getExecutionArgs()119         public List<String> getExecutionArgs() {
120             return args;
121         }
122 
123         @Override
storeLaunchArgs(List<String> args)124         public void storeLaunchArgs(List<String> args) {
125             try {
126                 patchScripts(this, args);
127             } catch (IOException ex) {
128                 throw new UncheckedIOException(ex);
129             }
130         }
131     }
132 
133     private final Path root;
134     private final Map<String, String> launchers;
135     private final Path mdir;
136     private final Set<String> modules = new HashSet<>();
137     private Platform targetPlatform;
138 
139     /**
140      * Default image builder constructor.
141      *
142      * @param root The image root directory.
143      * @throws IOException
144      */
DefaultImageBuilder(Path root, Map<String, String> launchers)145     public DefaultImageBuilder(Path root, Map<String, String> launchers) throws IOException {
146         this.root = Objects.requireNonNull(root);
147         this.launchers = Objects.requireNonNull(launchers);
148         this.mdir = root.resolve("lib");
149         Files.createDirectories(mdir);
150     }
151 
152     @Override
storeFiles(ResourcePool files)153     public void storeFiles(ResourcePool files) {
154         try {
155             String value = files.moduleView()
156                                 .findModule("java.base")
157                                 .map(ResourcePoolModule::targetPlatform)
158                                 .orElse(null);
159             if (value == null) {
160                 throw new PluginException("ModuleTarget attribute is missing for java.base module");
161             }
162             this.targetPlatform = Platform.toPlatform(value);
163 
164             checkResourcePool(files);
165 
166             Path bin = root.resolve(BIN_DIRNAME);
167 
168             // write non-classes resource files to the image
169             files.entries()
170                 .filter(f -> f.type() != ResourcePoolEntry.Type.CLASS_OR_RESOURCE)
171                 .forEach(f -> {
172                     try {
173                         accept(f);
174                     } catch (FileAlreadyExistsException e) {
175                         // Should not happen! Duplicates checking already done!
176                         throw new AssertionError("Duplicate entry!", e);
177                     } catch (IOException ioExp) {
178                         throw new UncheckedIOException(ioExp);
179                     }
180                 });
181 
182             files.moduleView().modules().forEach(m -> {
183                 // Only add modules that contain packages
184                 if (!m.packages().isEmpty()) {
185                     modules.add(m.name());
186                 }
187             });
188 
189             if (root.getFileSystem().supportedFileAttributeViews()
190                     .contains("posix")) {
191                 // launchers in the bin directory need execute permission.
192                 // On Windows, "bin" also subdirectories containing jvm.dll.
193                 if (Files.isDirectory(bin)) {
194                     Files.find(bin, 2, (path, attrs) -> {
195                         return attrs.isRegularFile() && !path.toString().endsWith(".diz");
196                     }).forEach(this::setExecutable);
197                 }
198 
199                 // jspawnhelper is in lib or lib/<arch>
200                 Path lib = root.resolve(LIB_DIRNAME);
201                 if (Files.isDirectory(lib)) {
202                     Files.find(lib, 2, (path, attrs) -> {
203                         return path.getFileName().toString().equals("jspawnhelper")
204                                 || path.getFileName().toString().equals("jexec");
205                     }).forEach(this::setExecutable);
206                 }
207 
208                 // read-only legal notices/license files
209                 Path legal = root.resolve(LEGAL_DIRNAME);
210                 if (Files.isDirectory(legal)) {
211                     Files.find(legal, 2, (path, attrs) -> {
212                         return attrs.isRegularFile();
213                     }).forEach(this::setReadOnly);
214                 }
215             }
216 
217             // If native files are stripped completely, <root>/bin dir won't exist!
218             // So, don't bother generating launcher scripts.
219             if (Files.isDirectory(bin)) {
220                  prepareApplicationFiles(files);
221             }
222         } catch (IOException ex) {
223             throw new PluginException(ex);
224         }
225     }
226 
checkResourcePool(ResourcePool pool)227     private void checkResourcePool(ResourcePool pool) {
228         // For now, only duplicate resources check. Add more checks here (if any)
229         checkDuplicateResources(pool);
230     }
231 
checkDuplicateResources(ResourcePool pool)232     private void checkDuplicateResources(ResourcePool pool) {
233         // check any duplicated resources
234         Map<Path, Set<String>> duplicates = new HashMap<>();
235         pool.entries()
236              .filter(f -> f.type() != ResourcePoolEntry.Type.CLASS_OR_RESOURCE)
237              .collect(groupingBy(this::entryToImagePath,
238                       mapping(ResourcePoolEntry::moduleName, toSet())))
239              .entrySet()
240              .stream()
241              .filter(e -> e.getValue().size() > 1)
242              .forEach(e -> duplicates.put(e.getKey(), e.getValue()));
243         if (!duplicates.isEmpty()) {
244             throw new PluginException("Duplicate resources: " + duplicates);
245         }
246     }
247 
248     /**
249      * Generates launcher scripts.
250      *
251      * @param imageContent The image content.
252      * @throws IOException
253      */
prepareApplicationFiles(ResourcePool imageContent)254     protected void prepareApplicationFiles(ResourcePool imageContent) throws IOException {
255         // generate launch scripts for the modules with a main class
256         for (Map.Entry<String, String> entry : launchers.entrySet()) {
257             String launcherEntry = entry.getValue();
258             int slashIdx = launcherEntry.indexOf("/");
259             String module, mainClassName;
260             if (slashIdx == -1) {
261                 module = launcherEntry;
262                 mainClassName = null;
263             } else {
264                 module = launcherEntry.substring(0, slashIdx);
265                 assert !module.isEmpty();
266                 mainClassName = launcherEntry.substring(slashIdx + 1);
267                 assert !mainClassName.isEmpty();
268             }
269 
270             if (mainClassName == null) {
271                 String path = "/" + module + "/module-info.class";
272                 Optional<ResourcePoolEntry> res = imageContent.findEntry(path);
273                 if (!res.isPresent()) {
274                     throw new IOException("module-info.class not found for " + module + " module");
275                 }
276                 ByteArrayInputStream stream = new ByteArrayInputStream(res.get().contentBytes());
277                 Optional<String> mainClass = ModuleDescriptor.read(stream).mainClass();
278                 if (mainClass.isPresent()) {
279                     mainClassName = mainClass.get();
280                 }
281             }
282 
283             if (mainClassName != null) {
284                 // make sure main class exists!
285                 if (!imageContent.findEntry("/" + module + "/" +
286                         mainClassName.replace('.', '/') + ".class").isPresent()) {
287                     throw new IllegalArgumentException(module + " does not have main class: " + mainClassName);
288                 }
289 
290                 String launcherFile = entry.getKey();
291                 Path cmd = root.resolve("bin").resolve(launcherFile);
292                 // generate shell script for Unix platforms
293                 StringBuilder sb = new StringBuilder();
294                 sb.append("#!/bin/sh")
295                         .append("\n");
296                 sb.append("JLINK_VM_OPTIONS=")
297                         .append("\n");
298                 sb.append("DIR=`dirname $0`")
299                         .append("\n");
300                 sb.append("$DIR/java $JLINK_VM_OPTIONS -m ")
301                         .append(module).append('/')
302                         .append(mainClassName)
303                         .append(" $@\n");
304 
305                 try (BufferedWriter writer = Files.newBufferedWriter(cmd,
306                         StandardCharsets.ISO_8859_1,
307                         StandardOpenOption.CREATE_NEW)) {
308                     writer.write(sb.toString());
309                 }
310                 if (root.resolve("bin").getFileSystem()
311                         .supportedFileAttributeViews().contains("posix")) {
312                     setExecutable(cmd);
313                 }
314                 // generate .bat file for Windows
315                 if (isWindows()) {
316                     Path bat = root.resolve(BIN_DIRNAME).resolve(launcherFile + ".bat");
317                     sb = new StringBuilder();
318                     sb.append("@echo off")
319                             .append("\r\n");
320                     sb.append("set JLINK_VM_OPTIONS=")
321                             .append("\r\n");
322                     sb.append("set DIR=%~dp0")
323                             .append("\r\n");
324                     sb.append("\"%DIR%\\java\" %JLINK_VM_OPTIONS% -m ")
325                             .append(module).append('/')
326                             .append(mainClassName)
327                             .append(" %*\r\n");
328 
329                     try (BufferedWriter writer = Files.newBufferedWriter(bat,
330                             StandardCharsets.ISO_8859_1,
331                             StandardOpenOption.CREATE_NEW)) {
332                         writer.write(sb.toString());
333                     }
334                 }
335             } else {
336                 throw new IllegalArgumentException(module + " doesn't contain main class & main not specified in command line");
337             }
338         }
339     }
340 
341     @Override
getJImageOutputStream()342     public DataOutputStream getJImageOutputStream() {
343         try {
344             Path jimageFile = mdir.resolve(BasicImageWriter.MODULES_IMAGE_NAME);
345             OutputStream fos = Files.newOutputStream(jimageFile);
346             BufferedOutputStream bos = new BufferedOutputStream(fos);
347             return new DataOutputStream(bos);
348         } catch (IOException ex) {
349             throw new UncheckedIOException(ex);
350         }
351     }
352 
353     /**
354      * Returns the file name of this entry
355      */
entryToFileName(ResourcePoolEntry entry)356     private String entryToFileName(ResourcePoolEntry entry) {
357         if (entry.type() == ResourcePoolEntry.Type.CLASS_OR_RESOURCE)
358             throw new IllegalArgumentException("invalid type: " + entry);
359 
360         String module = "/" + entry.moduleName() + "/";
361         String filename = entry.path().substring(module.length());
362 
363         // Remove radical lib|config|...
364         return filename.substring(filename.indexOf('/') + 1);
365     }
366 
367     /**
368      * Returns the path of the given entry to be written in the image
369      */
entryToImagePath(ResourcePoolEntry entry)370     private Path entryToImagePath(ResourcePoolEntry entry) {
371         switch (entry.type()) {
372             case NATIVE_LIB:
373                 String filename = entryToFileName(entry);
374                 return Paths.get(nativeDir(filename), filename);
375             case NATIVE_CMD:
376                 return Paths.get(BIN_DIRNAME, entryToFileName(entry));
377             case CONFIG:
378                 return Paths.get(CONF_DIRNAME, entryToFileName(entry));
379             case HEADER_FILE:
380                 return Paths.get(INCLUDE_DIRNAME, entryToFileName(entry));
381             case MAN_PAGE:
382                 return Paths.get(MAN_DIRNAME, entryToFileName(entry));
383             case LEGAL_NOTICE:
384                 return Paths.get(LEGAL_DIRNAME, entryToFileName(entry));
385             case TOP:
386                 return Paths.get(entryToFileName(entry));
387             default:
388                 throw new IllegalArgumentException("invalid type: " + entry);
389         }
390     }
391 
accept(ResourcePoolEntry file)392     private void accept(ResourcePoolEntry file) throws IOException {
393         if (file.linkedTarget() != null && file.type() != Type.LEGAL_NOTICE) {
394             throw new UnsupportedOperationException("symbolic link not implemented: " + file);
395         }
396 
397         try (InputStream in = file.content()) {
398             switch (file.type()) {
399                 case NATIVE_LIB:
400                     Path dest = root.resolve(entryToImagePath(file));
401                     writeEntry(in, dest);
402                     break;
403                 case NATIVE_CMD:
404                     Path p = root.resolve(entryToImagePath(file));
405                     writeEntry(in, p);
406                     p.toFile().setExecutable(true);
407                     break;
408                 case CONFIG:
409                 case HEADER_FILE:
410                 case MAN_PAGE:
411                     writeEntry(in, root.resolve(entryToImagePath(file)));
412                     break;
413                 case LEGAL_NOTICE:
414                     Path source = entryToImagePath(file);
415                     if (file.linkedTarget() == null) {
416                         writeEntry(in, root.resolve(source));
417                     } else {
418                         Path target = entryToImagePath(file.linkedTarget());
419                         Path relPath = source.getParent().relativize(target);
420                         writeSymLinkEntry(root.resolve(source), relPath);
421                     }
422                     break;
423                 case TOP:
424                     // Copy TOP files of the "java.base" module (only)
425                     if ("java.base".equals(file.moduleName())) {
426                         writeEntry(in, root.resolve(entryToImagePath(file)));
427                     } else {
428                         throw new InternalError("unexpected TOP entry: " + file.path());
429                     }
430                     break;
431                 default:
432                     throw new InternalError("unexpected entry: " + file.path());
433             }
434         }
435     }
436 
writeEntry(InputStream in, Path dstFile)437     private void writeEntry(InputStream in, Path dstFile) throws IOException {
438         Objects.requireNonNull(in);
439         Objects.requireNonNull(dstFile);
440         Files.createDirectories(Objects.requireNonNull(dstFile.getParent()));
441         Files.copy(in, dstFile);
442     }
443 
writeSymEntry(Path dstFile, Path target)444     private void writeSymEntry(Path dstFile, Path target) throws IOException {
445         Objects.requireNonNull(dstFile);
446         Objects.requireNonNull(target);
447         Files.createDirectories(Objects.requireNonNull(dstFile.getParent()));
448         Files.createLink(dstFile, target);
449     }
450 
451     /*
452      * Create a symbolic link to the given target if the target platform
453      * supports symbolic link; otherwise, it will create a tiny file
454      * to contain the path to the target.
455      */
writeSymLinkEntry(Path dstFile, Path target)456     private void writeSymLinkEntry(Path dstFile, Path target) throws IOException {
457         Objects.requireNonNull(dstFile);
458         Objects.requireNonNull(target);
459         Files.createDirectories(Objects.requireNonNull(dstFile.getParent()));
460         if (!isWindows() && root.getFileSystem()
461                                 .supportedFileAttributeViews()
462                                 .contains("posix")) {
463             Files.createSymbolicLink(dstFile, target);
464         } else {
465             try (BufferedWriter writer = Files.newBufferedWriter(dstFile)) {
466                 writer.write(String.format("Please see %s%n", target.toString()));
467             }
468         }
469     }
470 
nativeDir(String filename)471     private String nativeDir(String filename) {
472         if (isWindows()) {
473             if (filename.endsWith(".dll") || filename.endsWith(".diz")
474                     || filename.endsWith(".pdb") || filename.endsWith(".map")) {
475                 return BIN_DIRNAME;
476             } else {
477                 return LIB_DIRNAME;
478             }
479         } else {
480             return LIB_DIRNAME;
481         }
482     }
483 
isWindows()484     private boolean isWindows() {
485         return targetPlatform == Platform.WINDOWS;
486     }
487 
488     /**
489      * chmod ugo+x file
490      */
setExecutable(Path file)491     private void setExecutable(Path file) {
492         try {
493             Set<PosixFilePermission> perms = Files.getPosixFilePermissions(file);
494             perms.add(PosixFilePermission.OWNER_EXECUTE);
495             perms.add(PosixFilePermission.GROUP_EXECUTE);
496             perms.add(PosixFilePermission.OTHERS_EXECUTE);
497             Files.setPosixFilePermissions(file, perms);
498         } catch (IOException ioe) {
499             throw new UncheckedIOException(ioe);
500         }
501     }
502 
503     /**
504      * chmod ugo-w file
505      */
setReadOnly(Path file)506     private void setReadOnly(Path file) {
507         try {
508             Set<PosixFilePermission> perms = Files.getPosixFilePermissions(file);
509             perms.remove(PosixFilePermission.OWNER_WRITE);
510             perms.remove(PosixFilePermission.GROUP_WRITE);
511             perms.remove(PosixFilePermission.OTHERS_WRITE);
512             Files.setPosixFilePermissions(file, perms);
513         } catch (IOException ioe) {
514             throw new UncheckedIOException(ioe);
515         }
516     }
517 
createUtf8File(File file, String content)518     private static void createUtf8File(File file, String content) throws IOException {
519         try (OutputStream fout = new FileOutputStream(file);
520                 Writer output = new OutputStreamWriter(fout, "UTF-8")) {
521             output.write(content);
522         }
523     }
524 
525     @Override
getExecutableImage()526     public ExecutableImage getExecutableImage() {
527         return new DefaultExecutableImage(root, modules);
528     }
529 
530     // This is experimental, we should get rid-off the scripts in a near future
patchScripts(ExecutableImage img, List<String> args)531     private static void patchScripts(ExecutableImage img, List<String> args) throws IOException {
532         Objects.requireNonNull(args);
533         if (!args.isEmpty()) {
534             Files.find(img.getHome().resolve(BIN_DIRNAME), 2, (path, attrs) -> {
535                 return img.getModules().contains(path.getFileName().toString());
536             }).forEach((p) -> {
537                 try {
538                     String pattern = "JLINK_VM_OPTIONS=";
539                     byte[] content = Files.readAllBytes(p);
540                     String str = new String(content, StandardCharsets.UTF_8);
541                     int index = str.indexOf(pattern);
542                     StringBuilder builder = new StringBuilder();
543                     if (index != -1) {
544                         builder.append(str.substring(0, index)).
545                                 append(pattern);
546                         for (String s : args) {
547                             builder.append(s).append(" ");
548                         }
549                         String remain = str.substring(index + pattern.length());
550                         builder.append(remain);
551                         str = builder.toString();
552                         try (BufferedWriter writer = Files.newBufferedWriter(p,
553                                 StandardCharsets.ISO_8859_1,
554                                 StandardOpenOption.WRITE)) {
555                             writer.write(str);
556                         }
557                     }
558                 } catch (IOException ex) {
559                     throw new RuntimeException(ex);
560                 }
561             });
562         }
563     }
564 
getExecutableImage(Path root)565     public static ExecutableImage getExecutableImage(Path root) {
566         Path binDir = root.resolve(BIN_DIRNAME);
567         if (Files.exists(binDir.resolve("java")) ||
568             Files.exists(binDir.resolve("java.exe"))) {
569             return new DefaultExecutableImage(root, retrieveModules(root));
570         }
571         return null;
572     }
573 
retrieveModules(Path root)574     private static Set<String> retrieveModules(Path root) {
575         Path releaseFile = root.resolve("release");
576         Set<String> modules = new HashSet<>();
577         if (Files.exists(releaseFile)) {
578             Properties release = new Properties();
579             try (FileInputStream fi = new FileInputStream(releaseFile.toFile())) {
580                 release.load(fi);
581             } catch (IOException ex) {
582                 System.err.println("Can't read release file " + ex);
583             }
584             String mods = release.getProperty("MODULES");
585             if (mods != null) {
586                 String[] arr = mods.substring(1, mods.length() - 1).split(" ");
587                 for (String m : arr) {
588                     modules.add(m.trim());
589                 }
590 
591             }
592         }
593         return modules;
594     }
595 }
596