--- /dev/null 2019-02-15 09:11:05.676000757 +0100 +++ new/src/jdk.jlink/linux/classes/jdk/tools/jlink/internal/plugins/StripNativeDebugSymbolsPlugin.java 2019-02-15 17:54:58.126520451 +0100 @@ -0,0 +1,392 @@ +/* + * 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.IOException; +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.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Arrays; +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.PluginException; +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 DEFAULT_STRIP_CMD = "objcopy"; + private static final String STRIP_CMD_ARG = DEFAULT_STRIP_CMD; + private static final String KEEP_DEBUG_INFO_ARG = "keep-debuginfo"; + private static final String OMIT_DEBUG_INFO_ARG = "omit-debuginfo"; + private static final String DEFAULT_DEBUG_EXT = "debuginfo"; + 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 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 final ObjCopyCmdBuilder cmdBuilder; + private boolean includeDebugSymbols; + private String stripBin; + private String debuginfoExt; + + public StripNativeDebugSymbolsPlugin() { + this(new DefaultObjCopyCmdBuilder()); + } + + public StripNativeDebugSymbolsPlugin(ObjCopyCmdBuilder cmdBuilder) { + this.cmdBuilder = cmdBuilder; + } + + @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) { + doConfigure(true, config); + } + + // For testing so that validation can be turned off + public void doConfigure(boolean withChecks, Map config) { + String arg = config.get(NAME); + + stripBin = DEFAULT_STRIP_CMD; + debuginfoExt = DEFAULT_DEBUG_EXT; + + if (arg == null) { // will this ever be null? + return; + } + boolean hasOmitDebugInfo = false; + boolean hasKeepDebugInfo = false; + + if (KEEP_DEBUG_INFO_ARG.equals(arg)) { + // Case: --strip-native-debug-symbols keep-debug-info + hasKeepDebugInfo = true; + } else if (arg.startsWith(KEEP_DEBUG_INFO_ARG)) { + // Case: --strip-native-debug-symbols keep-debug-info=foo + String[] tokens = arg.split("="); + if (tokens.length != 2 || !KEEP_DEBUG_INFO_ARG.equals(tokens[0])) { + throw new IllegalArgumentException( + PluginsResourceBundle.getMessage(resourceBundle, + NAME + ".iae", NAME, arg)); + } + hasKeepDebugInfo = true; + debuginfoExt = tokens[1]; + } + if (OMIT_DEBUG_INFO_ARG.equals(arg)) { + // Case: --strip-native-debug-symbols omit-debug-info + hasOmitDebugInfo = true; + } + if (arg.startsWith(STRIP_CMD_ARG)) { + // Case: --strip-native-debug-symbols objcopy= + // --strip-native-debug-symbols keep-debug-info=ext:objcopy= + // --strip-native-debug-symbols omit-debug-info:objcopy= + String stripArg = config.get(STRIP_CMD_ARG); + if (stripArg != null && withChecks) { + validateStripArg(stripArg); + } + if (stripArg != null) { + stripBin = stripArg; + } + // Case (reversed combination) + // --strip-native-debug-symbols objcopy=:keep-debug-info=ext + // Note: cases like the following are not allowed by the parser + // --strip-native-debug-symbols objcopy=:keep-debug-info + // --strip-native-debug-symbols objcopy=:omit-debug-info + String keepDebugInfo = config.get(KEEP_DEBUG_INFO_ARG); + if (keepDebugInfo != null) { + hasKeepDebugInfo = true; + debuginfoExt = keepDebugInfo; + } + if (hasKeepDebugInfo && hasOmitDebugInfo) { + // cannot keep and omit debug info at the same time + throw new IllegalArgumentException( + PluginsResourceBundle.getMessage(resourceBundle, + NAME + ".iae.conflict", + NAME, + OMIT_DEBUG_INFO_ARG, + KEEP_DEBUG_INFO_ARG)); + } + includeDebugSymbols = hasKeepDebugInfo; + } + + 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; + + Files.write(resourceFileBinary, resource.contentBytes()); + 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 = Files.readAllBytes(resourceFileBinary); + return strippedBin; + } catch (IOException | InterruptedException e) { + throw new PluginException(e); + } 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 = buildCmdLine(STRIP_DEBUG_SYMS_OPT, + filePath); + ProcessBuilder builder = new ProcessBuilder(stripCmdLine); + Process stripProc = builder.start(); + int retval = stripProc.waitFor(); + return retval == 0; + } + + private List buildCmdLine(String ...opts) { + List objCopyCmd = cmdBuilder.build(stripBin); + objCopyCmd.addAll(Arrays.asList(opts)); + return objCopyCmd; + } + + 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 = buildCmdLine(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 = buildCmdLine(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 Files.readAllBytes(debugPath); + } + } + + 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; + } + + // For better testing using mocked objcopy + public static interface ObjCopyCmdBuilder { + List build(String objCopy); + } + + private static final class DefaultObjCopyCmdBuilder implements ObjCopyCmdBuilder { + + @Override + public List build(String objCopy) { + List cmdList = new ArrayList<>(); + cmdList.add(objCopy); + return cmdList; + } + + } + +}