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