/* * 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; } }