1 /* 2 * Copyright (c) 2019, Red Hat, Inc. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 package jdk.tools.jlink.internal.plugins; 26 27 import java.io.IOException; 28 import java.lang.ProcessBuilder.Redirect; 29 import java.nio.file.FileVisitResult; 30 import java.nio.file.Files; 31 import java.nio.file.InvalidPathException; 32 import java.nio.file.Path; 33 import java.nio.file.Paths; 34 import java.nio.file.SimpleFileVisitor; 35 import java.nio.file.attribute.BasicFileAttributes; 36 import java.util.ArrayList; 37 import java.util.Arrays; 38 import java.util.HashMap; 39 import java.util.List; 40 import java.util.Locale; 41 import java.util.Map; 42 import java.util.MissingResourceException; 43 import java.util.Objects; 44 import java.util.Optional; 45 import java.util.ResourceBundle; 46 47 import jdk.tools.jlink.plugin.Plugin; 48 import jdk.tools.jlink.plugin.PluginException; 49 import jdk.tools.jlink.plugin.ResourcePool; 50 import jdk.tools.jlink.plugin.ResourcePoolBuilder; 51 import jdk.tools.jlink.plugin.ResourcePoolEntry; 52 53 /** 54 * Platform specific jlink plugin for stripping debug symbols from native 55 * libraries and binaries. 56 * 57 */ 58 public final class StripNativeDebugSymbolsPlugin implements Plugin { 59 60 public static final String NAME = "strip-native-debug-symbols"; 61 private static final boolean DEBUG = Boolean.getBoolean("jlink.debug"); 62 private static final String DEFAULT_STRIP_CMD = "objcopy"; 63 private static final String STRIP_CMD_ARG = DEFAULT_STRIP_CMD; 64 private static final String KEEP_DEBUG_INFO_ARG = "keep-debuginfo-files"; 65 private static final String EXCLUDE_DEBUG_INFO_ARG = "exclude-debuginfo-files"; 66 private static final String DEFAULT_DEBUG_EXT = "debuginfo"; 67 private static final String STRIP_DEBUG_SYMS_OPT = "-g"; 68 private static final String ONLY_KEEP_DEBUG_SYMS_OPT = "--only-keep-debug"; 69 private static final String ADD_DEBUG_LINK_OPT = "--add-gnu-debuglink"; 70 private static final ResourceBundle resourceBundle; 71 private static final String SHARED_LIBS_EXT = ".so"; // for Linux/Unix 72 73 static { 74 Locale locale = Locale.getDefault(); 75 try { 76 resourceBundle = ResourceBundle.getBundle("jdk.tools.jlink." 77 + "resources.strip_native_debug_symbols_plugin", locale); 78 } catch (MissingResourceException e) { 79 throw new InternalError("Cannot find jlink plugin resource bundle (" + 80 NAME + ") for locale " + locale); 81 } 82 } 83 84 private final ObjCopyCmdBuilder cmdBuilder; 85 private boolean includeDebugSymbols; 86 private String stripBin; 87 private String debuginfoExt; 88 StripNativeDebugSymbolsPlugin()89 public StripNativeDebugSymbolsPlugin() { 90 this(new DefaultObjCopyCmdBuilder()); 91 } 92 StripNativeDebugSymbolsPlugin(ObjCopyCmdBuilder cmdBuilder)93 public StripNativeDebugSymbolsPlugin(ObjCopyCmdBuilder cmdBuilder) { 94 this.cmdBuilder = cmdBuilder; 95 } 96 97 @Override getName()98 public String getName() { 99 return NAME; 100 } 101 102 @Override transform(ResourcePool in, ResourcePoolBuilder out)103 public ResourcePool transform(ResourcePool in, ResourcePoolBuilder out) { 104 StrippedDebugInfoBinaryBuilder builder = new StrippedDebugInfoBinaryBuilder( 105 includeDebugSymbols, 106 debuginfoExt, 107 cmdBuilder, 108 stripBin); 109 in.transformAndCopy((resource) -> { 110 ResourcePoolEntry res = resource; 111 if ((resource.type() == ResourcePoolEntry.Type.NATIVE_LIB && 112 resource.path().endsWith(SHARED_LIBS_EXT)) || 113 resource.type() == ResourcePoolEntry.Type.NATIVE_CMD) { 114 Optional<StrippedDebugInfoBinary> strippedBin = builder.build(resource); 115 if (strippedBin.isPresent()) { 116 StrippedDebugInfoBinary sb = strippedBin.get(); 117 res = sb.strippedBinary(); 118 if (includeDebugSymbols) { 119 Optional<ResourcePoolEntry> debugInfo = sb.debugSymbols(); 120 if (debugInfo.isEmpty()) { 121 String key = NAME + ".error.debugfile"; 122 logError(resource, key); 123 } else { 124 out.add(debugInfo.get()); 125 } 126 } 127 } else { 128 String key = NAME + ".error.file"; 129 logError(resource, key); 130 } 131 } 132 return res; 133 }, out); 134 135 return out.build(); 136 } 137 logError(ResourcePoolEntry resource, String msgKey)138 private void logError(ResourcePoolEntry resource, String msgKey) { 139 String msg = PluginsResourceBundle.getMessage(resourceBundle, 140 msgKey, 141 NAME, 142 resource.path()); 143 System.err.println(msg); 144 } 145 146 @Override getType()147 public Category getType() { 148 return Category.TRANSFORMER; 149 } 150 151 @Override getDescription()152 public String getDescription() { 153 String key = NAME + ".description"; 154 return PluginsResourceBundle.getMessage(resourceBundle, key); 155 } 156 157 @Override hasArguments()158 public boolean hasArguments() { 159 return true; 160 } 161 162 @Override getArgumentsDescription()163 public String getArgumentsDescription() { 164 String key = NAME + ".argument"; 165 return PluginsResourceBundle.getMessage(resourceBundle, key); 166 } 167 168 @Override configure(Map<String, String> config)169 public void configure(Map<String, String> config) { 170 doConfigure(true, config); 171 } 172 173 // For testing so that validation can be turned off doConfigure(boolean withChecks, Map<String, String> orig)174 public void doConfigure(boolean withChecks, Map<String, String> orig) { 175 Map<String, String> config = new HashMap<>(orig); 176 String arg = config.remove(NAME); 177 178 stripBin = DEFAULT_STRIP_CMD; 179 debuginfoExt = DEFAULT_DEBUG_EXT; 180 181 // argument must never be null as it requires at least one 182 // argument, since hasArguments() == true. This might change once 183 // 8218761 is implemented. 184 if (arg == null) { 185 throw new InternalError(); 186 } 187 boolean hasOmitDebugInfo = false; 188 boolean hasKeepDebugInfo = false; 189 190 if (KEEP_DEBUG_INFO_ARG.equals(arg)) { 191 // Case: --strip-native-debug-symbols keep-debuginfo-files 192 hasKeepDebugInfo = true; 193 } else if (arg.startsWith(KEEP_DEBUG_INFO_ARG)) { 194 // Case: --strip-native-debug-symbols keep-debuginfo-files=foo 195 String[] tokens = arg.split("="); 196 if (tokens.length != 2 || !KEEP_DEBUG_INFO_ARG.equals(tokens[0])) { 197 throw new IllegalArgumentException( 198 PluginsResourceBundle.getMessage(resourceBundle, 199 NAME + ".iae", NAME, arg)); 200 } 201 hasKeepDebugInfo = true; 202 debuginfoExt = tokens[1]; 203 } 204 if (EXCLUDE_DEBUG_INFO_ARG.equals(arg) || arg.startsWith(EXCLUDE_DEBUG_INFO_ARG + "=")) { 205 // Case: --strip-native-debug-symbols exclude-debuginfo-files[=something] 206 hasOmitDebugInfo = true; 207 } 208 if (arg.startsWith(STRIP_CMD_ARG)) { 209 // Case: --strip-native-debug-symbols objcopy=<path/to/objcopy 210 String[] tokens = arg.split("="); 211 if (tokens.length != 2 || !STRIP_CMD_ARG.equals(tokens[0])) { 212 throw new IllegalArgumentException( 213 PluginsResourceBundle.getMessage(resourceBundle, 214 NAME + ".iae", NAME, arg)); 215 } 216 if (withChecks) { 217 validateStripArg(tokens[1]); 218 } 219 stripBin = tokens[1]; 220 } 221 // Cases (combination of options): 222 // --strip-native-debug-symbols keep-debuginfo-files:objcopy=</objcpy/path> 223 // --strip-native-debug-symbols keep-debuginfo-files=ext:objcopy=</objcpy/path> 224 // --strip-native-debug-symbols exclude-debuginfo-files:objcopy=</objcpy/path> 225 String stripArg = config.remove(STRIP_CMD_ARG); 226 if (stripArg != null && withChecks) { 227 validateStripArg(stripArg); 228 } 229 if (stripArg != null) { 230 stripBin = stripArg; 231 } 232 // Case (reversed combination) 233 // --strip-native-debug-symbols objcopy=</objcpy/path>:keep-debuginfo-files=ext 234 // Note: cases like the following are not allowed by the parser 235 // --strip-native-debug-symbols objcopy=</objcpy/path>:keep-debuginfo-files 236 // --strip-native-debug-symbols objcopy=</objcpy/path>:exclude-debuginfo-files 237 String keepDebugInfo = config.remove(KEEP_DEBUG_INFO_ARG); 238 if (keepDebugInfo != null) { 239 hasKeepDebugInfo = true; 240 debuginfoExt = keepDebugInfo; 241 } 242 if ((hasKeepDebugInfo || includeDebugSymbols) && hasOmitDebugInfo) { 243 // Cannot keep and omit debug info at the same time. Note that 244 // includeDebugSymbols might already be true if configure is being run 245 // on the same plugin instance multiple times. Plugin option can 246 // repeat. 247 throw new IllegalArgumentException( 248 PluginsResourceBundle.getMessage(resourceBundle, 249 NAME + ".iae.conflict", 250 NAME, 251 EXCLUDE_DEBUG_INFO_ARG, 252 KEEP_DEBUG_INFO_ARG)); 253 } 254 if (!arg.startsWith(STRIP_CMD_ARG) && 255 !arg.startsWith(KEEP_DEBUG_INFO_ARG) && 256 !arg.startsWith(EXCLUDE_DEBUG_INFO_ARG)) { 257 // unknown arg value; case --strip-native-debug-symbols foobar 258 throw new IllegalArgumentException( 259 PluginsResourceBundle.getMessage(resourceBundle, 260 NAME + ".iae", NAME, arg)); 261 } 262 if (!config.isEmpty()) { 263 // extraneous values; --strip-native-debug-symbols keep-debuginfo-files:foo=bar 264 throw new IllegalArgumentException( 265 PluginsResourceBundle.getMessage(resourceBundle, 266 NAME + ".iae", NAME, 267 config.toString())); 268 } 269 includeDebugSymbols = hasKeepDebugInfo; 270 } 271 validateStripArg(String stripArg)272 private void validateStripArg(String stripArg) throws IllegalArgumentException { 273 try { 274 Path strip = Paths.get(stripArg); // verify it's a resonable path 275 if (!Files.isExecutable(strip)) { 276 throw new IllegalArgumentException( 277 PluginsResourceBundle.getMessage(resourceBundle, 278 NAME + ".invalidstrip", 279 stripArg)); 280 } 281 } catch (InvalidPathException e) { 282 throw new IllegalArgumentException( 283 PluginsResourceBundle.getMessage(resourceBundle, 284 NAME + ".invalidstrip", 285 e.getInput())); 286 } 287 } 288 289 private static class StrippedDebugInfoBinaryBuilder { 290 291 private final boolean includeDebug; 292 private final String debugExt; 293 private final ObjCopyCmdBuilder cmdBuilder; 294 private final String strip; 295 StrippedDebugInfoBinaryBuilder(boolean includeDebug, String debugExt, ObjCopyCmdBuilder cmdBuilder, String strip)296 private StrippedDebugInfoBinaryBuilder(boolean includeDebug, 297 String debugExt, 298 ObjCopyCmdBuilder cmdBuilder, 299 String strip) { 300 this.includeDebug = includeDebug; 301 this.debugExt = debugExt; 302 this.cmdBuilder = cmdBuilder; 303 this.strip = strip; 304 } 305 build(ResourcePoolEntry resource)306 private Optional<StrippedDebugInfoBinary> build(ResourcePoolEntry resource) { 307 Path tempDir = null; 308 Optional<ResourcePoolEntry> debugInfo = Optional.empty(); 309 try { 310 Path resPath = Paths.get(resource.path()); 311 String relativeFileName = resPath.getFileName().toString(); 312 tempDir = Files.createTempDirectory(NAME + relativeFileName); 313 Path resourceFileBinary = tempDir.resolve(relativeFileName); 314 String relativeDbgFileName = relativeFileName + "." + debugExt; 315 316 Files.write(resourceFileBinary, resource.contentBytes()); 317 Path resourceFileDebugSymbols; 318 if (includeDebug) { 319 resourceFileDebugSymbols = tempDir.resolve(Paths.get(relativeDbgFileName)); 320 String debugEntryPath = resource.path() + "." + debugExt; 321 byte[] debugInfoBytes = createDebugSymbolsFile(resourceFileBinary, 322 resourceFileDebugSymbols, 323 relativeDbgFileName); 324 if (debugInfoBytes != null) { 325 ResourcePoolEntry debugEntry = ResourcePoolEntry.create( 326 debugEntryPath, 327 resource.type(), 328 debugInfoBytes); 329 debugInfo = Optional.of(debugEntry); 330 } 331 } 332 if (!stripBinary(resourceFileBinary)) { 333 if (DEBUG) { 334 System.err.println("DEBUG: Stripping debug info failed."); 335 } 336 return Optional.empty(); 337 } 338 if (includeDebug && !addGnuDebugLink(tempDir, 339 relativeFileName, 340 relativeDbgFileName)) { 341 if (DEBUG) { 342 System.err.println("DEBUG: Creating debug link failed."); 343 } 344 return Optional.empty(); 345 } 346 byte[] strippedBytes = Files.readAllBytes(resourceFileBinary); 347 ResourcePoolEntry strippedResource = resource.copyWithContent(strippedBytes); 348 return Optional.of(new StrippedDebugInfoBinary(strippedResource, debugInfo)); 349 } catch (IOException | InterruptedException e) { 350 throw new PluginException(e); 351 } finally { 352 if (tempDir != null) { 353 deleteDirRecursivelyIgnoreResult(tempDir); 354 } 355 } 356 } 357 358 /* 359 * Equivalent of 'objcopy -g binFile'. Returning true iff stripping of the binary 360 * succeeded. 361 */ stripBinary(Path binFile)362 private boolean stripBinary(Path binFile) 363 throws InterruptedException, IOException { 364 String filePath = binFile.toAbsolutePath().toString(); 365 List<String> stripCmdLine = cmdBuilder.build(strip, STRIP_DEBUG_SYMS_OPT, 366 filePath); 367 ProcessBuilder builder = createProcessBuilder(stripCmdLine); 368 Process stripProc = builder.start(); 369 int retval = stripProc.waitFor(); 370 return retval == 0; 371 } 372 373 /* 374 * Equivalent of 'objcopy --add-gnu-debuglink=relativeDbgFileName binFile'. 375 * Returning true iff adding the debug link succeeded. 376 */ addGnuDebugLink(Path currDir, String binFile, String relativeDbgFileName)377 private boolean addGnuDebugLink(Path currDir, 378 String binFile, 379 String relativeDbgFileName) 380 throws InterruptedException, IOException { 381 List<String> addDbgLinkCmdLine = cmdBuilder.build(strip, ADD_DEBUG_LINK_OPT + 382 "=" + relativeDbgFileName, 383 binFile); 384 ProcessBuilder builder = createProcessBuilder(addDbgLinkCmdLine); 385 builder.directory(currDir.toFile()); 386 Process stripProc = builder.start(); 387 int retval = stripProc.waitFor(); 388 return retval == 0; 389 390 } 391 392 /* 393 * Equivalent of 'objcopy --only-keep-debug binPath debugPath'. 394 * Returning the bytes of the file containing debug symbols. 395 */ createDebugSymbolsFile(Path binPath, Path debugPath, String dbgFileName)396 private byte[] createDebugSymbolsFile(Path binPath, 397 Path debugPath, 398 String dbgFileName) throws InterruptedException, 399 IOException { 400 String filePath = binPath.toAbsolutePath().toString(); 401 String dbgPath = debugPath.toAbsolutePath().toString(); 402 List<String> createLinkCmdLine = cmdBuilder.build(strip, 403 ONLY_KEEP_DEBUG_SYMS_OPT, 404 filePath, 405 dbgPath); 406 ProcessBuilder builder = createProcessBuilder(createLinkCmdLine); 407 Process stripProc = builder.start(); 408 int retval = stripProc.waitFor(); 409 if (retval != 0) { 410 if (DEBUG) { 411 System.err.println("DEBUG: Creating debuginfo file failed."); 412 } 413 return null; 414 } else { 415 return Files.readAllBytes(debugPath); 416 } 417 } 418 createProcessBuilder(List<String> cmd)419 private ProcessBuilder createProcessBuilder(List<String> cmd) { 420 ProcessBuilder builder = new ProcessBuilder(cmd); 421 builder.redirectError(Redirect.INHERIT); 422 builder.redirectOutput(Redirect.INHERIT); 423 return builder; 424 } 425 deleteDirRecursivelyIgnoreResult(Path tempDir)426 private void deleteDirRecursivelyIgnoreResult(Path tempDir) { 427 try { 428 Files.walkFileTree(tempDir, new SimpleFileVisitor<Path>() { 429 @Override 430 public FileVisitResult visitFile(Path file, 431 BasicFileAttributes attrs) throws IOException { 432 Files.delete(file); 433 return FileVisitResult.CONTINUE; 434 } 435 436 @Override 437 public FileVisitResult postVisitDirectory(Path dir, 438 IOException exc) throws IOException { 439 Files.delete(dir); 440 return FileVisitResult.CONTINUE; 441 } 442 }); 443 } catch (IOException e) { 444 // ignore deleting the temp dir 445 } 446 } 447 448 } 449 450 private static class StrippedDebugInfoBinary { 451 private final ResourcePoolEntry strippedBinary; 452 private final Optional<ResourcePoolEntry> debugSymbols; 453 StrippedDebugInfoBinary(ResourcePoolEntry strippedBinary, Optional<ResourcePoolEntry> debugSymbols)454 private StrippedDebugInfoBinary(ResourcePoolEntry strippedBinary, 455 Optional<ResourcePoolEntry> debugSymbols) { 456 this.strippedBinary = Objects.requireNonNull(strippedBinary); 457 this.debugSymbols = Objects.requireNonNull(debugSymbols); 458 } 459 strippedBinary()460 public ResourcePoolEntry strippedBinary() { 461 return strippedBinary; 462 } 463 debugSymbols()464 public Optional<ResourcePoolEntry> debugSymbols() { 465 return debugSymbols; 466 } 467 } 468 469 // For better testing using mocked objcopy 470 public static interface ObjCopyCmdBuilder { build(String objCopy, String...options)471 List<String> build(String objCopy, String...options); 472 } 473 474 private static final class DefaultObjCopyCmdBuilder implements ObjCopyCmdBuilder { 475 476 @Override build(String objCopy, String...options)477 public List<String> build(String objCopy, String...options) { 478 List<String> cmdList = new ArrayList<>(); 479 cmdList.add(objCopy); 480 if (options.length > 0) { 481 cmdList.addAll(Arrays.asList(options)); 482 } 483 return cmdList; 484 } 485 486 } 487 488 } 489