| /* |
| * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Oracle designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Oracle in the LICENSE file that accompanied this code. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| package jdk.tools.jlink.builder; |
| |
| import java.io.BufferedOutputStream; |
| import java.io.BufferedWriter; |
| import java.io.ByteArrayInputStream; |
| import java.io.DataOutputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.io.OutputStreamWriter; |
| import java.io.UncheckedIOException; |
| import java.io.Writer; |
| import java.lang.module.ModuleDescriptor; |
| import java.nio.charset.StandardCharsets; |
| import java.nio.file.FileAlreadyExistsException; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.nio.file.StandardOpenOption; |
| import java.nio.file.attribute.PosixFilePermission; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Optional; |
| import java.util.Properties; |
| import java.util.Set; |
| import static java.util.stream.Collectors.*; |
| |
| import jdk.tools.jlink.internal.BasicImageWriter; |
| import jdk.tools.jlink.internal.ExecutableImage; |
| import jdk.tools.jlink.plugin.ResourcePool; |
| import jdk.tools.jlink.plugin.ResourcePoolEntry; |
| import jdk.tools.jlink.plugin.ResourcePoolEntry.Type; |
| import jdk.tools.jlink.plugin.ResourcePoolModule; |
| import jdk.tools.jlink.plugin.PluginException; |
| |
| /** |
| * |
| * Default Image Builder. This builder creates the default runtime image layout. |
| */ |
| public final class DefaultImageBuilder implements ImageBuilder { |
| // Top-level directory names in a modular runtime image |
| public static final String BIN_DIRNAME = "bin"; |
| public static final String CONF_DIRNAME = "conf"; |
| public static final String INCLUDE_DIRNAME = "include"; |
| public static final String LIB_DIRNAME = "lib"; |
| public static final String LEGAL_DIRNAME = "legal"; |
| public static final String MAN_DIRNAME = "man"; |
| |
| /** |
| * The default java executable Image. |
| */ |
| static final class DefaultExecutableImage implements ExecutableImage { |
| |
| private final Path home; |
| private final List<String> args; |
| private final Set<String> modules; |
| |
| DefaultExecutableImage(Path home, Set<String> modules) { |
| Objects.requireNonNull(home); |
| if (!Files.exists(home)) { |
| throw new IllegalArgumentException("Invalid image home"); |
| } |
| this.home = home; |
| this.modules = Collections.unmodifiableSet(modules); |
| this.args = createArgs(home); |
| } |
| |
| private static List<String> createArgs(Path home) { |
| Objects.requireNonNull(home); |
| Path binDir = home.resolve("bin"); |
| String java = Files.exists(binDir.resolve("java"))? "java" : "java.exe"; |
| return List.of(binDir.resolve(java).toString()); |
| } |
| |
| @Override |
| public Path getHome() { |
| return home; |
| } |
| |
| @Override |
| public Set<String> getModules() { |
| return modules; |
| } |
| |
| @Override |
| public List<String> getExecutionArgs() { |
| return args; |
| } |
| |
| @Override |
| public void storeLaunchArgs(List<String> args) { |
| try { |
| patchScripts(this, args); |
| } catch (IOException ex) { |
| throw new UncheckedIOException(ex); |
| } |
| } |
| } |
| |
| private final Path root; |
| private final Map<String, String> launchers; |
| private final Path mdir; |
| private final Set<String> modules = new HashSet<>(); |
| private String targetOsName; |
| |
| /** |
| * Default image builder constructor. |
| * |
| * @param root The image root directory. |
| * @throws IOException |
| */ |
| public DefaultImageBuilder(Path root, Map<String, String> launchers) throws IOException { |
| this.root = Objects.requireNonNull(root); |
| this.launchers = Objects.requireNonNull(launchers); |
| this.mdir = root.resolve("lib"); |
| Files.createDirectories(mdir); |
| } |
| |
| @Override |
| public void storeFiles(ResourcePool files) { |
| try { |
| // populate targetOsName field up-front because it's used elsewhere. |
| Optional<ResourcePoolModule> javaBase = files.moduleView().findModule("java.base"); |
| javaBase.ifPresent(mod -> { |
| // fill release information available from transformed "java.base" module! |
| ModuleDescriptor desc = mod.descriptor(); |
| desc.osName().ifPresent(s -> { |
| this.targetOsName = s; |
| }); |
| }); |
| |
| if (this.targetOsName == null) { |
| throw new PluginException("ModuleTarget attribute is missing for java.base module"); |
| } |
| |
| checkResourcePool(files); |
| |
| Path bin = root.resolve(BIN_DIRNAME); |
| |
| // write non-classes resource files to the image |
| files.entries() |
| .filter(f -> f.type() != ResourcePoolEntry.Type.CLASS_OR_RESOURCE) |
| .forEach(f -> { |
| try { |
| accept(f); |
| } catch (FileAlreadyExistsException e) { |
| // Should not happen! Duplicates checking already done! |
| throw new AssertionError("Duplicate entry!", e); |
| } catch (IOException ioExp) { |
| throw new UncheckedIOException(ioExp); |
| } |
| }); |
| |
| files.moduleView().modules().forEach(m -> { |
| // Only add modules that contain packages |
| if (!m.packages().isEmpty()) { |
| modules.add(m.name()); |
| } |
| }); |
| |
| if (root.getFileSystem().supportedFileAttributeViews() |
| .contains("posix")) { |
| // launchers in the bin directory need execute permission. |
| // On Windows, "bin" also subdirectories containing jvm.dll. |
| if (Files.isDirectory(bin)) { |
| Files.find(bin, 2, (path, attrs) -> { |
| return attrs.isRegularFile() && !path.toString().endsWith(".diz"); |
| }).forEach(this::setExecutable); |
| } |
| |
| // jspawnhelper is in lib or lib/<arch> |
| Path lib = root.resolve(LIB_DIRNAME); |
| if (Files.isDirectory(lib)) { |
| Files.find(lib, 2, (path, attrs) -> { |
| return path.getFileName().toString().equals("jspawnhelper") |
| || path.getFileName().toString().equals("jexec"); |
| }).forEach(this::setExecutable); |
| } |
| |
| // read-only legal notices/license files |
| Path legal = root.resolve(LEGAL_DIRNAME); |
| if (Files.isDirectory(legal)) { |
| Files.find(legal, 2, (path, attrs) -> { |
| return attrs.isRegularFile(); |
| }).forEach(this::setReadOnly); |
| } |
| } |
| |
| // If native files are stripped completely, <root>/bin dir won't exist! |
| // So, don't bother generating launcher scripts. |
| if (Files.isDirectory(bin)) { |
| prepareApplicationFiles(files); |
| } |
| } catch (IOException ex) { |
| throw new PluginException(ex); |
| } |
| } |
| |
| private void checkResourcePool(ResourcePool pool) { |
| // For now, only duplicate resources check. Add more checks here (if any) |
| checkDuplicateResources(pool); |
| } |
| |
| private void checkDuplicateResources(ResourcePool pool) { |
| // check any duplicated resources |
| Map<Path, Set<String>> duplicates = new HashMap<>(); |
| pool.entries() |
| .filter(f -> f.type() != ResourcePoolEntry.Type.CLASS_OR_RESOURCE) |
| .collect(groupingBy(this::entryToImagePath, |
| mapping(ResourcePoolEntry::moduleName, toSet()))) |
| .entrySet() |
| .stream() |
| .filter(e -> e.getValue().size() > 1) |
| .forEach(e -> duplicates.put(e.getKey(), e.getValue())); |
| if (!duplicates.isEmpty()) { |
| throw new PluginException("Duplicate resources: " + duplicates); |
| } |
| } |
| |
| /** |
| * Generates launcher scripts. |
| * |
| * @param imageContent The image content. |
| * @throws IOException |
| */ |
| protected void prepareApplicationFiles(ResourcePool imageContent) throws IOException { |
| // generate launch scripts for the modules with a main class |
| for (Map.Entry<String, String> entry : launchers.entrySet()) { |
| String launcherEntry = entry.getValue(); |
| int slashIdx = launcherEntry.indexOf("/"); |
| String module, mainClassName; |
| if (slashIdx == -1) { |
| module = launcherEntry; |
| mainClassName = null; |
| } else { |
| module = launcherEntry.substring(0, slashIdx); |
| assert !module.isEmpty(); |
| mainClassName = launcherEntry.substring(slashIdx + 1); |
| assert !mainClassName.isEmpty(); |
| } |
| |
| String path = "/" + module + "/module-info.class"; |
| Optional<ResourcePoolEntry> res = imageContent.findEntry(path); |
| if (!res.isPresent()) { |
| throw new IOException("module-info.class not found for " + module + " module"); |
| } |
| ByteArrayInputStream stream = new ByteArrayInputStream(res.get().contentBytes()); |
| Optional<String> mainClass = ModuleDescriptor.read(stream).mainClass(); |
| if (mainClassName == null && mainClass.isPresent()) { |
| mainClassName = mainClass.get(); |
| } |
| |
| if (mainClassName != null) { |
| // make sure main class exists! |
| if (!imageContent.findEntry("/" + module + "/" + |
| mainClassName.replace('.', '/') + ".class").isPresent()) { |
| throw new IllegalArgumentException(module + " does not have main class: " + mainClassName); |
| } |
| |
| String launcherFile = entry.getKey(); |
| Path cmd = root.resolve("bin").resolve(launcherFile); |
| // generate shell script for Unix platforms |
| StringBuilder sb = new StringBuilder(); |
| sb.append("#!/bin/sh") |
| .append("\n"); |
| sb.append("JLINK_VM_OPTIONS=") |
| .append("\n"); |
| sb.append("DIR=`dirname $0`") |
| .append("\n"); |
| sb.append("$DIR/java $JLINK_VM_OPTIONS -m ") |
| .append(module).append('/') |
| .append(mainClassName) |
| .append(" $@\n"); |
| |
| try (BufferedWriter writer = Files.newBufferedWriter(cmd, |
| StandardCharsets.ISO_8859_1, |
| StandardOpenOption.CREATE_NEW)) { |
| writer.write(sb.toString()); |
| } |
| if (root.resolve("bin").getFileSystem() |
| .supportedFileAttributeViews().contains("posix")) { |
| setExecutable(cmd); |
| } |
| // generate .bat file for Windows |
| if (isWindows()) { |
| Path bat = root.resolve(BIN_DIRNAME).resolve(launcherFile + ".bat"); |
| sb = new StringBuilder(); |
| sb.append("@echo off") |
| .append("\r\n"); |
| sb.append("set JLINK_VM_OPTIONS=") |
| .append("\r\n"); |
| sb.append("set DIR=%~dp0") |
| .append("\r\n"); |
| sb.append("\"%DIR%\\java\" %JLINK_VM_OPTIONS% -m ") |
| .append(module).append('/') |
| .append(mainClassName) |
| .append(" %*\r\n"); |
| |
| try (BufferedWriter writer = Files.newBufferedWriter(bat, |
| StandardCharsets.ISO_8859_1, |
| StandardOpenOption.CREATE_NEW)) { |
| writer.write(sb.toString()); |
| } |
| } |
| } else { |
| throw new IllegalArgumentException(module + " doesn't contain main class & main not specified in command line"); |
| } |
| } |
| } |
| |
| @Override |
| public DataOutputStream getJImageOutputStream() { |
| try { |
| Path jimageFile = mdir.resolve(BasicImageWriter.MODULES_IMAGE_NAME); |
| OutputStream fos = Files.newOutputStream(jimageFile); |
| BufferedOutputStream bos = new BufferedOutputStream(fos); |
| return new DataOutputStream(bos); |
| } catch (IOException ex) { |
| throw new UncheckedIOException(ex); |
| } |
| } |
| |
| /** |
| * Returns the file name of this entry |
| */ |
| private String entryToFileName(ResourcePoolEntry entry) { |
| if (entry.type() == ResourcePoolEntry.Type.CLASS_OR_RESOURCE) |
| throw new IllegalArgumentException("invalid type: " + entry); |
| |
| String module = "/" + entry.moduleName() + "/"; |
| String filename = entry.path().substring(module.length()); |
| |
| // Remove radical native|config|... |
| return filename.substring(filename.indexOf('/') + 1); |
| } |
| |
| /** |
| * Returns the path of the given entry to be written in the image |
| */ |
| private Path entryToImagePath(ResourcePoolEntry entry) { |
| switch (entry.type()) { |
| case NATIVE_LIB: |
| String filename = entryToFileName(entry); |
| return Paths.get(nativeDir(filename), filename); |
| case NATIVE_CMD: |
| return Paths.get(BIN_DIRNAME, entryToFileName(entry)); |
| case CONFIG: |
| return Paths.get(CONF_DIRNAME, entryToFileName(entry)); |
| case HEADER_FILE: |
| return Paths.get(INCLUDE_DIRNAME, entryToFileName(entry)); |
| case MAN_PAGE: |
| return Paths.get(MAN_DIRNAME, entryToFileName(entry)); |
| case LEGAL_NOTICE: |
| return Paths.get(LEGAL_DIRNAME, entryToFileName(entry)); |
| case TOP: |
| return Paths.get(entryToFileName(entry)); |
| default: |
| throw new IllegalArgumentException("invalid type: " + entry); |
| } |
| } |
| |
| private void accept(ResourcePoolEntry file) throws IOException { |
| if (file.linkedTarget() != null && file.type() != Type.LEGAL_NOTICE) { |
| throw new UnsupportedOperationException("symbolic link not implemented: " + file); |
| } |
| |
| try (InputStream in = file.content()) { |
| switch (file.type()) { |
| case NATIVE_LIB: |
| Path dest = root.resolve(entryToImagePath(file)); |
| writeEntry(in, dest); |
| break; |
| case NATIVE_CMD: |
| Path p = root.resolve(entryToImagePath(file)); |
| writeEntry(in, p); |
| p.toFile().setExecutable(true); |
| break; |
| case CONFIG: |
| case HEADER_FILE: |
| case MAN_PAGE: |
| writeEntry(in, root.resolve(entryToImagePath(file))); |
| break; |
| case LEGAL_NOTICE: |
| Path source = entryToImagePath(file); |
| if (file.linkedTarget() == null) { |
| writeEntry(in, root.resolve(source)); |
| } else { |
| Path target = entryToImagePath(file.linkedTarget()); |
| Path relPath = source.getParent().relativize(target); |
| writeSymLinkEntry(root.resolve(source), relPath); |
| } |
| break; |
| case TOP: |
| // Copy TOP files of the "java.base" module (only) |
| if ("java.base".equals(file.moduleName())) { |
| writeEntry(in, root.resolve(entryToImagePath(file))); |
| } else { |
| throw new InternalError("unexpected TOP entry: " + file.path()); |
| } |
| break; |
| default: |
| throw new InternalError("unexpected entry: " + file.path()); |
| } |
| } |
| } |
| |
| private void writeEntry(InputStream in, Path dstFile) throws IOException { |
| Objects.requireNonNull(in); |
| Objects.requireNonNull(dstFile); |
| Files.createDirectories(Objects.requireNonNull(dstFile.getParent())); |
| Files.copy(in, dstFile); |
| } |
| |
| private void writeSymEntry(Path dstFile, Path target) throws IOException { |
| Objects.requireNonNull(dstFile); |
| Objects.requireNonNull(target); |
| Files.createDirectories(Objects.requireNonNull(dstFile.getParent())); |
| Files.createLink(dstFile, target); |
| } |
| |
| /* |
| * Create a symbolic link to the given target if the target platform |
| * supports symbolic link; otherwise, it will create a tiny file |
| * to contain the path to the target. |
| */ |
| private void writeSymLinkEntry(Path dstFile, Path target) throws IOException { |
| Objects.requireNonNull(dstFile); |
| Objects.requireNonNull(target); |
| Files.createDirectories(Objects.requireNonNull(dstFile.getParent())); |
| if (!isWindows() && root.getFileSystem() |
| .supportedFileAttributeViews() |
| .contains("posix")) { |
| Files.createSymbolicLink(dstFile, target); |
| } else { |
| try (BufferedWriter writer = Files.newBufferedWriter(dstFile)) { |
| writer.write(String.format("Please see %s%n", target.toString())); |
| } |
| } |
| } |
| |
| private String nativeDir(String filename) { |
| if (isWindows()) { |
| if (filename.endsWith(".dll") || filename.endsWith(".diz") |
| || filename.endsWith(".pdb") || filename.endsWith(".map")) { |
| return BIN_DIRNAME; |
| } else { |
| return LIB_DIRNAME; |
| } |
| } else { |
| return LIB_DIRNAME; |
| } |
| } |
| |
| private boolean isWindows() { |
| return targetOsName.startsWith("Windows"); |
| } |
| |
| /** |
| * chmod ugo+x file |
| */ |
| private void setExecutable(Path file) { |
| try { |
| Set<PosixFilePermission> perms = Files.getPosixFilePermissions(file); |
| perms.add(PosixFilePermission.OWNER_EXECUTE); |
| perms.add(PosixFilePermission.GROUP_EXECUTE); |
| perms.add(PosixFilePermission.OTHERS_EXECUTE); |
| Files.setPosixFilePermissions(file, perms); |
| } catch (IOException ioe) { |
| throw new UncheckedIOException(ioe); |
| } |
| } |
| |
| /** |
| * chmod ugo-w file |
| */ |
| private void setReadOnly(Path file) { |
| try { |
| Set<PosixFilePermission> perms = Files.getPosixFilePermissions(file); |
| perms.remove(PosixFilePermission.OWNER_WRITE); |
| perms.remove(PosixFilePermission.GROUP_WRITE); |
| perms.remove(PosixFilePermission.OTHERS_WRITE); |
| Files.setPosixFilePermissions(file, perms); |
| } catch (IOException ioe) { |
| throw new UncheckedIOException(ioe); |
| } |
| } |
| |
| private static void createUtf8File(File file, String content) throws IOException { |
| try (OutputStream fout = new FileOutputStream(file); |
| Writer output = new OutputStreamWriter(fout, "UTF-8")) { |
| output.write(content); |
| } |
| } |
| |
| @Override |
| public ExecutableImage getExecutableImage() { |
| return new DefaultExecutableImage(root, modules); |
| } |
| |
| // This is experimental, we should get rid-off the scripts in a near future |
| private static void patchScripts(ExecutableImage img, List<String> args) throws IOException { |
| Objects.requireNonNull(args); |
| if (!args.isEmpty()) { |
| Files.find(img.getHome().resolve(BIN_DIRNAME), 2, (path, attrs) -> { |
| return img.getModules().contains(path.getFileName().toString()); |
| }).forEach((p) -> { |
| try { |
| String pattern = "JLINK_VM_OPTIONS="; |
| byte[] content = Files.readAllBytes(p); |
| String str = new String(content, StandardCharsets.UTF_8); |
| int index = str.indexOf(pattern); |
| StringBuilder builder = new StringBuilder(); |
| if (index != -1) { |
| builder.append(str.substring(0, index)). |
| append(pattern); |
| for (String s : args) { |
| builder.append(s).append(" "); |
| } |
| String remain = str.substring(index + pattern.length()); |
| builder.append(remain); |
| str = builder.toString(); |
| try (BufferedWriter writer = Files.newBufferedWriter(p, |
| StandardCharsets.ISO_8859_1, |
| StandardOpenOption.WRITE)) { |
| writer.write(str); |
| } |
| } |
| } catch (IOException ex) { |
| throw new RuntimeException(ex); |
| } |
| }); |
| } |
| } |
| |
| public static ExecutableImage getExecutableImage(Path root) { |
| Path binDir = root.resolve(BIN_DIRNAME); |
| if (Files.exists(binDir.resolve("java")) || |
| Files.exists(binDir.resolve("java.exe"))) { |
| return new DefaultExecutableImage(root, retrieveModules(root)); |
| } |
| return null; |
| } |
| |
| private static Set<String> retrieveModules(Path root) { |
| Path releaseFile = root.resolve("release"); |
| Set<String> modules = new HashSet<>(); |
| if (Files.exists(releaseFile)) { |
| Properties release = new Properties(); |
| try (FileInputStream fi = new FileInputStream(releaseFile.toFile())) { |
| release.load(fi); |
| } catch (IOException ex) { |
| System.err.println("Can't read release file " + ex); |
| } |
| String mods = release.getProperty("MODULES"); |
| if (mods != null) { |
| String[] arr = mods.substring(1, mods.length() - 1).split(" "); |
| for (String m : arr) { |
| modules.add(m.trim()); |
| } |
| |
| } |
| } |
| return modules; |
| } |
| } |