src/jdk.jlink/linux/classes/jdk/tools/jlink/internal/plugins/StripNativeDebugSymbolsPlugin.java
8214796: Create a jlink plugin for stripping debug info symbols from native libraries
Reviewed-by: alanb, mchung, erikj, ihse
/*
* 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.lang.ProcessBuilder.Redirect;
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.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Objects;
import java.util.Optional;
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 boolean DEBUG = Boolean.getBoolean("jlink.debug");
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-files";
private static final String EXCLUDE_DEBUG_INFO_ARG = "exclude-debuginfo-files";
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) {
StrippedDebugInfoBinaryBuilder builder = new StrippedDebugInfoBinaryBuilder(
includeDebugSymbols,
debuginfoExt,
cmdBuilder,
stripBin);
in.transformAndCopy((resource) -> {
ResourcePoolEntry res = resource;
if ((resource.type() == ResourcePoolEntry.Type.NATIVE_LIB &&
resource.path().endsWith(SHARED_LIBS_EXT)) ||
resource.type() == ResourcePoolEntry.Type.NATIVE_CMD) {
Optional<StrippedDebugInfoBinary> strippedBin = builder.build(resource);
if (strippedBin.isPresent()) {
StrippedDebugInfoBinary sb = strippedBin.get();
res = sb.strippedBinary();
if (includeDebugSymbols) {
Optional<ResourcePoolEntry> debugInfo = sb.debugSymbols();
if (debugInfo.isEmpty()) {
String key = NAME + ".error.debugfile";
logError(resource, key);
} else {
out.add(debugInfo.get());
}
}
} else {
String key = NAME + ".error.file";
logError(resource, key);
}
}
return res;
}, out);
return out.build();
}
private void logError(ResourcePoolEntry resource, String msgKey) {
String msg = PluginsResourceBundle.getMessage(resourceBundle,
msgKey,
NAME,
resource.path());
System.err.println(msg);
}
@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);
}
@Override
public void configure(Map<String, String> config) {
doConfigure(true, config);
}
// For testing so that validation can be turned off
public void doConfigure(boolean withChecks, Map<String, String> orig) {
Map<String, String> config = new HashMap<>(orig);
String arg = config.remove(NAME);
stripBin = DEFAULT_STRIP_CMD;
debuginfoExt = DEFAULT_DEBUG_EXT;
// argument must never be null as it requires at least one
// argument, since hasArguments() == true. This might change once
// 8218761 is implemented.
if (arg == null) {
throw new InternalError();
}
boolean hasOmitDebugInfo = false;
boolean hasKeepDebugInfo = false;
if (KEEP_DEBUG_INFO_ARG.equals(arg)) {
// Case: --strip-native-debug-symbols keep-debuginfo-files
hasKeepDebugInfo = true;
} else if (arg.startsWith(KEEP_DEBUG_INFO_ARG)) {
// Case: --strip-native-debug-symbols keep-debuginfo-files=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 (EXCLUDE_DEBUG_INFO_ARG.equals(arg) || arg.startsWith(EXCLUDE_DEBUG_INFO_ARG + "=")) {
// Case: --strip-native-debug-symbols exclude-debuginfo-files[=something]
hasOmitDebugInfo = true;
}
if (arg.startsWith(STRIP_CMD_ARG)) {
// Case: --strip-native-debug-symbols objcopy=<path/to/objcopy
String[] tokens = arg.split("=");
if (tokens.length != 2 || !STRIP_CMD_ARG.equals(tokens[0])) {
throw new IllegalArgumentException(
PluginsResourceBundle.getMessage(resourceBundle,
NAME + ".iae", NAME, arg));
}
if (withChecks) {
validateStripArg(tokens[1]);
}
stripBin = tokens[1];
}
// Cases (combination of options):
// --strip-native-debug-symbols keep-debuginfo-files:objcopy=</objcpy/path>
// --strip-native-debug-symbols keep-debuginfo-files=ext:objcopy=</objcpy/path>
// --strip-native-debug-symbols exclude-debuginfo-files:objcopy=</objcpy/path>
String stripArg = config.remove(STRIP_CMD_ARG);
if (stripArg != null && withChecks) {
validateStripArg(stripArg);
}
if (stripArg != null) {
stripBin = stripArg;
}
// Case (reversed combination)
// --strip-native-debug-symbols objcopy=</objcpy/path>:keep-debuginfo-files=ext
// Note: cases like the following are not allowed by the parser
// --strip-native-debug-symbols objcopy=</objcpy/path>:keep-debuginfo-files
// --strip-native-debug-symbols objcopy=</objcpy/path>:exclude-debuginfo-files
String keepDebugInfo = config.remove(KEEP_DEBUG_INFO_ARG);
if (keepDebugInfo != null) {
hasKeepDebugInfo = true;
debuginfoExt = keepDebugInfo;
}
if ((hasKeepDebugInfo || includeDebugSymbols) && hasOmitDebugInfo) {
// Cannot keep and omit debug info at the same time. Note that
// includeDebugSymbols might already be true if configure is being run
// on the same plugin instance multiple times. Plugin option can
// repeat.
throw new IllegalArgumentException(
PluginsResourceBundle.getMessage(resourceBundle,
NAME + ".iae.conflict",
NAME,
EXCLUDE_DEBUG_INFO_ARG,
KEEP_DEBUG_INFO_ARG));
}
if (!arg.startsWith(STRIP_CMD_ARG) &&
!arg.startsWith(KEEP_DEBUG_INFO_ARG) &&
!arg.startsWith(EXCLUDE_DEBUG_INFO_ARG)) {
// unknown arg value; case --strip-native-debug-symbols foobar
throw new IllegalArgumentException(
PluginsResourceBundle.getMessage(resourceBundle,
NAME + ".iae", NAME, arg));
}
if (!config.isEmpty()) {
// extraneous values; --strip-native-debug-symbols keep-debuginfo-files:foo=bar
throw new IllegalArgumentException(
PluginsResourceBundle.getMessage(resourceBundle,
NAME + ".iae", NAME,
config.toString()));
}
includeDebugSymbols = hasKeepDebugInfo;
}
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 StrippedDebugInfoBinaryBuilder {
private final boolean includeDebug;
private final String debugExt;
private final ObjCopyCmdBuilder cmdBuilder;
private final String strip;
private StrippedDebugInfoBinaryBuilder(boolean includeDebug,
String debugExt,
ObjCopyCmdBuilder cmdBuilder,
String strip) {
this.includeDebug = includeDebug;
this.debugExt = debugExt;
this.cmdBuilder = cmdBuilder;
this.strip = strip;
}
private Optional<StrippedDebugInfoBinary> build(ResourcePoolEntry resource) {
Path tempDir = null;
Optional<ResourcePoolEntry> debugInfo = Optional.empty();
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 + "." + debugExt;
Files.write(resourceFileBinary, resource.contentBytes());
Path resourceFileDebugSymbols;
if (includeDebug) {
resourceFileDebugSymbols = tempDir.resolve(Paths.get(relativeDbgFileName));
String debugEntryPath = resource.path() + "." + debugExt;
byte[] debugInfoBytes = createDebugSymbolsFile(resourceFileBinary,
resourceFileDebugSymbols,
relativeDbgFileName);
if (debugInfoBytes != null) {
ResourcePoolEntry debugEntry = ResourcePoolEntry.create(
debugEntryPath,
resource.type(),
debugInfoBytes);
debugInfo = Optional.of(debugEntry);
}
}
if (!stripBinary(resourceFileBinary)) {
if (DEBUG) {
System.err.println("DEBUG: Stripping debug info failed.");
}
return Optional.empty();
}
if (includeDebug && !addGnuDebugLink(tempDir,
relativeFileName,
relativeDbgFileName)) {
if (DEBUG) {
System.err.println("DEBUG: Creating debug link failed.");
}
return Optional.empty();
}
byte[] strippedBytes = Files.readAllBytes(resourceFileBinary);
ResourcePoolEntry strippedResource = resource.copyWithContent(strippedBytes);
return Optional.of(new StrippedDebugInfoBinary(strippedResource, debugInfo));
} 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.toAbsolutePath().toString();
List<String> stripCmdLine = cmdBuilder.build(strip, STRIP_DEBUG_SYMS_OPT,
filePath);
ProcessBuilder builder = createProcessBuilder(stripCmdLine);
Process stripProc = builder.start();
int retval = stripProc.waitFor();
return retval == 0;
}
/*
* 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<String> addDbgLinkCmdLine = cmdBuilder.build(strip, ADD_DEBUG_LINK_OPT +
"=" + relativeDbgFileName,
binFile);
ProcessBuilder builder = createProcessBuilder(addDbgLinkCmdLine);
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.toAbsolutePath().toString();
String dbgPath = debugPath.toAbsolutePath().toString();
List<String> createLinkCmdLine = cmdBuilder.build(strip,
ONLY_KEEP_DEBUG_SYMS_OPT,
filePath,
dbgPath);
ProcessBuilder builder = createProcessBuilder(createLinkCmdLine);
Process stripProc = builder.start();
int retval = stripProc.waitFor();
if (retval != 0) {
if (DEBUG) {
System.err.println("DEBUG: Creating debuginfo file failed.");
}
return null;
} else {
return Files.readAllBytes(debugPath);
}
}
private ProcessBuilder createProcessBuilder(List<String> cmd) {
ProcessBuilder builder = new ProcessBuilder(cmd);
builder.redirectError(Redirect.INHERIT);
builder.redirectOutput(Redirect.INHERIT);
return builder;
}
private void deleteDirRecursivelyIgnoreResult(Path tempDir) {
try {
Files.walkFileTree(tempDir, new SimpleFileVisitor<Path>() {
@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
}
}
}
private static class StrippedDebugInfoBinary {
private final ResourcePoolEntry strippedBinary;
private final Optional<ResourcePoolEntry> debugSymbols;
private StrippedDebugInfoBinary(ResourcePoolEntry strippedBinary,
Optional<ResourcePoolEntry> debugSymbols) {
this.strippedBinary = Objects.requireNonNull(strippedBinary);
this.debugSymbols = Objects.requireNonNull(debugSymbols);
}
public ResourcePoolEntry strippedBinary() {
return strippedBinary;
}
public Optional<ResourcePoolEntry> debugSymbols() {
return debugSymbols;
}
}
// For better testing using mocked objcopy
public static interface ObjCopyCmdBuilder {
List<String> build(String objCopy, String...options);
}
private static final class DefaultObjCopyCmdBuilder implements ObjCopyCmdBuilder {
@Override
public List<String> build(String objCopy, String...options) {
List<String> cmdList = new ArrayList<>();
cmdList.add(objCopy);
if (options.length > 0) {
cmdList.addAll(Arrays.asList(options));
}
return cmdList;
}
}
}