--- /dev/null 2019-02-07 10:57:10.613999929 +0100 +++ new/src/jdk.jlink/unix/classes/jdk/tools/jlink/internal/plugins/StripNativeDebugSymbolsPlugin.java 2019-02-07 17:49:17.407599396 +0100 @@ -0,0 +1,313 @@ +/* + * Copyright (c) 2019, Red Hat, Inc. + * 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.internal.plugins; + +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.MissingResourceException; +import java.util.ResourceBundle; + +import jdk.tools.jlink.plugin.Plugin; +import jdk.tools.jlink.plugin.ResourcePool; +import jdk.tools.jlink.plugin.ResourcePoolBuilder; +import jdk.tools.jlink.plugin.ResourcePoolEntry; + +/** + * Platform specific jlink plugin for stripping debug symbols from native + * libraries and binaries. + * + */ +public final class StripNativeDebugSymbolsPlugin implements Plugin { + + public static final String NAME = "strip-native-debug-symbols"; + private static final String STRIP_CMD_ARG = "objcopy-cmd"; + private static final String INCLUDE_DBG_SYMS_ARG = "include-debug-syms"; + private static final String DEBUG_EXT_ARG = "debuginfo-file-ext"; + private static final String DEFAULT_DEBUG_EXT = "debuginfo"; + private static final String DEFAULT_STRIP_CMD = "objcopy"; + private static final String STRIP_DEBUG_SYMS_OPT = "-g"; + private static final String ONLY_KEEP_DEBUG_SYMS_OPT = "--only-keep-debug"; + private static final String ADD_DEBUG_LINK_OPT = "--add-gnu-debuglink"; + private static final String ALL_DEFAULTS = "defaults"; + private static final String USE_OPTIONS = "options"; + private static final ResourceBundle resourceBundle; + private static final String SHARED_LIBS_EXT = ".so"; // for Linux/Unix + + static { + Locale locale = Locale.getDefault(); + try { + resourceBundle = ResourceBundle.getBundle("jdk.tools.jlink." + + "resources.strip_native_debug_symbols_plugin", locale); + } catch (MissingResourceException e) { + throw new InternalError("Cannot find jlink plugin resource bundle (" + NAME + ") for " + + "locale " + locale); + } + } + + private boolean includeDebugSymbols; + private String stripBin; + private String debuginfoExt; + + @Override + public String getName() { + return NAME; + } + + @Override + public ResourcePool transform(ResourcePool in, ResourcePoolBuilder out) { + in.transformAndCopy((resource) -> { + ResourcePoolEntry res = resource; + if (resource.type() == ResourcePoolEntry.Type.NATIVE_LIB || + resource.type() == ResourcePoolEntry.Type.NATIVE_CMD) { + String moduleName = resource.moduleName(); + if (resource.path().endsWith(SHARED_LIBS_EXT) || + resource.path().startsWith("/" + moduleName + "/bin")) { + StrippedDebugInfoBinary strippedBin = stripBinary(resource); + if (strippedBin != null) { + res = resource.copyWithContent(strippedBin.strippedBinary); + if (includeDebugSymbols) { + String debugEntryPath = resource.path() + "." + debuginfoExt; + ResourcePoolEntry debugEntry = ResourcePoolEntry.create(debugEntryPath, resource.type(), strippedBin.debugSymbols); + out.add(debugEntry); + } + } + } + } + return res; + }, out); + + return out.build(); + } + + @Override + public Category getType() { + return Category.TRANSFORMER; + } + + @Override + public String getDescription() { + String key = NAME + ".description"; + return PluginsResourceBundle.getMessage(resourceBundle, key); + } + + @Override + public boolean hasArguments() { + return true; + } + + @Override + public String getArgumentsDescription() { + String key = NAME + ".argument"; + return PluginsResourceBundle.getMessage(resourceBundle, key); + } + + // --strip-native-debug-symbols= + @Override + public void configure(Map config) { + String arg = config.get(NAME); + + debuginfoExt = DEFAULT_DEBUG_EXT; + stripBin = DEFAULT_STRIP_CMD; + + if (arg == null) { // will this ever be null? + return; + } + if (USE_OPTIONS.equals(arg)) { + String dbgExt = config.get(DEBUG_EXT_ARG); + if (dbgExt != null) { + debuginfoExt = dbgExt; + } + String stripArg = config.get(STRIP_CMD_ARG); + if (stripArg != null) { + validateStripArg(stripArg); + stripBin = stripArg; + } + String includeDbgSymbols = config.get(INCLUDE_DBG_SYMS_ARG); + includeDebugSymbols = Boolean.parseBoolean(includeDbgSymbols); + } else if (!ALL_DEFAULTS.equals(arg)) { + throw new IllegalArgumentException( + PluginsResourceBundle.getMessage(resourceBundle, NAME + ".iae", NAME, arg)); + } + } + + private StrippedDebugInfoBinary stripBinary(ResourcePoolEntry resource) { + Path tempDir = null; + try { + Path resPath = Paths.get(resource.path()); + String relativeFileName = resPath.getFileName().toString(); + tempDir = Files.createTempDirectory(NAME + relativeFileName); + Path resourceFileBinary = tempDir.resolve(relativeFileName); + String relativeDbgFileName = relativeFileName + "." + debuginfoExt; + + ByteArrayInputStream bin = new ByteArrayInputStream(resource.contentBytes()); + Files.copy(bin, resourceFileBinary, StandardCopyOption.REPLACE_EXISTING); + StrippedDebugInfoBinary strippedBin = new StrippedDebugInfoBinary(); + Path resourceFileDebugSymbols; + if (includeDebugSymbols) { + resourceFileDebugSymbols = tempDir.resolve(Paths.get(relativeDbgFileName)); + strippedBin.debugSymbols = createDebugSymbolsFile(resourceFileBinary, + resourceFileDebugSymbols, + relativeDbgFileName); + } + if (!stripBinary(resourceFileBinary)) { + return null; + } + if (includeDebugSymbols && !addGnuDebugLink(tempDir, relativeFileName, relativeDbgFileName)) { + return null; + } + strippedBin.strippedBinary = readFileBytes(resourceFileBinary); + return strippedBin; + } catch (Exception e) { + return null; + } finally { + if (tempDir != null) { + deleteDirRecursivelyIgnoreResult(tempDir); + } + } + } + + /* + * Equivalent of 'objcopy -g binFile'. Returning true iff stripping of the binary + * succeeded. + */ + private boolean stripBinary(Path binFile) + throws InterruptedException, IOException { + String filePath = binFile.toFile().getAbsolutePath(); + List stripCmdLine = List. of(stripBin, STRIP_DEBUG_SYMS_OPT, filePath); + ProcessBuilder builder = new ProcessBuilder(stripCmdLine); + Process stripProc = builder.start(); + int retval = stripProc.waitFor(); + return retval == 0; + } + + private void deleteDirRecursivelyIgnoreResult(Path tempDir) { + try { + Files.walkFileTree(tempDir, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, + BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, + IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + // ignore deleting the temp dir + } + } + + /* + * Equivalent of 'objcopy --add-gnu-debuglink=relativeDbgFileName binFile'. + * Returning true iff adding the debug link succeeded. + */ + private boolean addGnuDebugLink(Path currDir, String binFile, String relativeDbgFileName) throws InterruptedException, IOException { + List stripCmdLine = List.of(stripBin, ADD_DEBUG_LINK_OPT + "=" + relativeDbgFileName, binFile); + ProcessBuilder builder = new ProcessBuilder(stripCmdLine); + builder.directory(currDir.toFile()); + Process stripProc = builder.start(); + int retval = stripProc.waitFor(); + return retval == 0; + + } + + /* + * Equivalent of 'objcopy --only-keep-debug binPath debugPath'. + * Returning the bytes of the file containing debug symbols. + */ + private byte[] createDebugSymbolsFile(Path binPath, + Path debugPath, + String dbgFileName) throws InterruptedException, + IOException { + String filePath = binPath.toFile().getAbsolutePath(); + String dbgPath = debugPath.toFile().getAbsolutePath(); + List stripCmdLine = List. of(stripBin, ONLY_KEEP_DEBUG_SYMS_OPT, filePath, dbgPath); + ProcessBuilder builder = new ProcessBuilder(stripCmdLine); + Process stripProc = builder.start(); + int retval = stripProc.waitFor(); + if (retval != 0) { + return null; + } else { + return readFileBytes(debugPath); + } + } + + private byte[] readFileBytes(Path resourceFile) { + long size = resourceFile.toFile().length(); + if (size > Integer.MAX_VALUE) { + return null; // file too big + } + byte[] fileBytes = new byte[(int) size]; + byte[] buf = new byte[1024]; + int bytesRead = -1; + int count = 0; + try (InputStream in = new FileInputStream(resourceFile.toFile())) { + while ((bytesRead = in.read(buf)) != -1) { + System.arraycopy(buf, 0, fileBytes, count, bytesRead); + count += bytesRead; + } + return fileBytes; + } catch (Exception e) { + return null; + } + } + + private void validateStripArg(String stripArg) throws IllegalArgumentException { + try { + Path strip = Paths.get(stripArg); // verify it's a resonable path + if (!Files.isExecutable(strip)) { + throw new IllegalArgumentException(PluginsResourceBundle + .getMessage(resourceBundle, NAME + ".invalidstrip", stripArg)); + } + } catch (InvalidPathException e) { + throw new IllegalArgumentException(PluginsResourceBundle + .getMessage(resourceBundle, NAME + ".invalidstrip", e.getInput())); + } + } + + private static class StrippedDebugInfoBinary { + byte[] strippedBinary; + byte[] debugSymbols; + } + +}