package jdk.tools.jlink.internal.plugins;
import java.io.InputStream;
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;
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";
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);
}
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;
if (arg == null) {
throw new InternalError();
}
boolean hasOmitDebugInfo = false;
boolean hasKeepDebugInfo = false;
if (KEEP_DEBUG_INFO_ARG.equals(arg)) {
hasKeepDebugInfo = true;
} else if (arg.startsWith(KEEP_DEBUG_INFO_ARG)) {
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 + "=")) {
hasOmitDebugInfo = true;
}
if (arg.startsWith(STRIP_CMD_ARG)) {
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];
}
String stripArg = config.remove(STRIP_CMD_ARG);
if (stripArg != null && withChecks) {
validateStripArg(stripArg);
}
if (stripArg != null) {
stripBin = stripArg;
}
String keepDebugInfo = config.remove(KEEP_DEBUG_INFO_ARG);
if (keepDebugInfo != null) {
hasKeepDebugInfo = true;
debuginfoExt = keepDebugInfo;
}
if ((hasKeepDebugInfo || includeDebugSymbols) && hasOmitDebugInfo) {
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)) {
throw new IllegalArgumentException(
PluginsResourceBundle.getMessage(resourceBundle,
NAME + ".iae", NAME, arg));
}
if (!config.isEmpty()) {
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);
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;
try (InputStream in = resource.content()) {
Files.copy(in, resourceFileBinary);
}
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);
}
}
}
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;
}
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;
}
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) {
}
}
}
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;
}
}
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;
}
}
}