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.nio.file.FileVisitResult; 29 import java.nio.file.Files; 30 import java.nio.file.InvalidPathException; 31 import java.nio.file.Path; 32 import java.nio.file.Paths; 33 import java.nio.file.SimpleFileVisitor; 34 import java.nio.file.attribute.BasicFileAttributes; 35 import java.util.ArrayList; 36 import java.util.Arrays; 37 import java.util.List; 38 import java.util.Locale; 39 import java.util.Map; 40 import java.util.MissingResourceException; 41 import java.util.ResourceBundle; 42 43 import jdk.tools.jlink.plugin.Plugin; 44 import jdk.tools.jlink.plugin.PluginException; 45 import jdk.tools.jlink.plugin.ResourcePool; 46 import jdk.tools.jlink.plugin.ResourcePoolBuilder; 47 import jdk.tools.jlink.plugin.ResourcePoolEntry; 48 49 /** 50 * Platform specific jlink plugin for stripping debug symbols from native 51 * libraries and binaries. 52 * 53 */ 54 public final class StripNativeDebugSymbolsPlugin implements Plugin { 55 56 public static final String NAME = "strip-native-debug-symbols"; 57 private static final String DEFAULT_STRIP_CMD = "objcopy"; 58 private static final String STRIP_CMD_ARG = DEFAULT_STRIP_CMD; 59 private static final String KEEP_DEBUG_INFO_ARG = "keep-debuginfo"; 60 private static final String OMIT_DEBUG_INFO_ARG = "omit-debuginfo"; 61 private static final String DEFAULT_DEBUG_EXT = "debuginfo"; 62 private static final String STRIP_DEBUG_SYMS_OPT = "-g"; 63 private static final String ONLY_KEEP_DEBUG_SYMS_OPT = "--only-keep-debug"; 64 private static final String ADD_DEBUG_LINK_OPT = "--add-gnu-debuglink"; 65 private static final ResourceBundle resourceBundle; 66 private static final String SHARED_LIBS_EXT = ".so"; // for Linux/Unix 67 68 static { 69 Locale locale = Locale.getDefault(); 70 try { 71 resourceBundle = ResourceBundle.getBundle("jdk.tools.jlink." 72 + "resources.strip_native_debug_symbols_plugin", locale); 73 } catch (MissingResourceException e) { 74 throw new InternalError("Cannot find jlink plugin resource bundle (" + 75 NAME + ") for locale " + locale); 76 } 77 } 78 79 private final ObjCopyCmdBuilder cmdBuilder; 80 private boolean includeDebugSymbols; 81 private String stripBin; 82 private String debuginfoExt; 83 84 public StripNativeDebugSymbolsPlugin() { 85 this(new DefaultObjCopyCmdBuilder()); 86 } 87 88 public StripNativeDebugSymbolsPlugin(ObjCopyCmdBuilder cmdBuilder) { 89 this.cmdBuilder = cmdBuilder; 90 } 91 92 @Override 93 public String getName() { 94 return NAME; 95 } 96 97 @Override 98 public ResourcePool transform(ResourcePool in, ResourcePoolBuilder out) { 99 in.transformAndCopy((resource) -> { 100 ResourcePoolEntry res = resource; 101 if (resource.type() == ResourcePoolEntry.Type.NATIVE_LIB || 102 resource.type() == ResourcePoolEntry.Type.NATIVE_CMD) { 103 String moduleName = resource.moduleName(); 104 if (resource.path().endsWith(SHARED_LIBS_EXT) || 105 resource.path().startsWith("/" + moduleName + "/bin")) { 106 StrippedDebugInfoBinary strippedBin = stripBinary(resource); 107 if (strippedBin != null) { 108 res = resource.copyWithContent(strippedBin.strippedBinary); 109 if (includeDebugSymbols) { 110 String debugEntryPath = resource.path() + 111 "." + debuginfoExt; 112 ResourcePoolEntry debugEntry = ResourcePoolEntry.create( 113 debugEntryPath, 114 resource.type(), 115 strippedBin.debugSymbols); 116 out.add(debugEntry); 117 } 118 } 119 } 120 } 121 return res; 122 }, out); 123 124 return out.build(); 125 } 126 127 @Override 128 public Category getType() { 129 return Category.TRANSFORMER; 130 } 131 132 @Override 133 public String getDescription() { 134 String key = NAME + ".description"; 135 return PluginsResourceBundle.getMessage(resourceBundle, key); 136 } 137 138 @Override 139 public boolean hasArguments() { 140 return true; 141 } 142 143 @Override 144 public String getArgumentsDescription() { 145 String key = NAME + ".argument"; 146 return PluginsResourceBundle.getMessage(resourceBundle, key); 147 } 148 149 // --strip-native-debug-symbols=<defaults|options[:objcopy-cmd=/usr/bin/objcopy][:debuginfo-file-ext=dbg][:include-debug-syms=true]> 150 @Override 151 public void configure(Map<String, String> config) { 152 doConfigure(true, config); 153 } 154 155 // For testing so that validation can be turned off 156 public void doConfigure(boolean withChecks, Map<String, String> config) { 157 String arg = config.get(NAME); 158 159 stripBin = DEFAULT_STRIP_CMD; 160 debuginfoExt = DEFAULT_DEBUG_EXT; 161 162 if (arg == null) { // will this ever be null? 163 return; 164 } 165 boolean hasOmitDebugInfo = false; 166 boolean hasKeepDebugInfo = false; 167 168 if (KEEP_DEBUG_INFO_ARG.equals(arg)) { 169 // Case: --strip-native-debug-symbols keep-debug-info 170 hasKeepDebugInfo = true; 171 } else if (arg.startsWith(KEEP_DEBUG_INFO_ARG)) { 172 // Case: --strip-native-debug-symbols keep-debug-info=foo 173 String[] tokens = arg.split("="); 174 if (tokens.length != 2 || !KEEP_DEBUG_INFO_ARG.equals(tokens[0])) { 175 throw new IllegalArgumentException( 176 PluginsResourceBundle.getMessage(resourceBundle, 177 NAME + ".iae", NAME, arg)); 178 } 179 hasKeepDebugInfo = true; 180 debuginfoExt = tokens[1]; 181 } 182 if (OMIT_DEBUG_INFO_ARG.equals(arg)) { 183 // Case: --strip-native-debug-symbols omit-debug-info 184 hasOmitDebugInfo = true; 185 } 186 if (arg.startsWith(STRIP_CMD_ARG)) { 187 // Case: --strip-native-debug-symbols objcopy=<path/to/objcopy 188 String[] tokens = arg.split("="); 189 if (tokens.length != 2 || !STRIP_CMD_ARG.equals(tokens[0])) { 190 throw new IllegalArgumentException( 191 PluginsResourceBundle.getMessage(resourceBundle, 192 NAME + ".iae", NAME, arg)); 193 } 194 if (withChecks) { 195 validateStripArg(tokens[1]); 196 } 197 stripBin = tokens[1]; 198 } 199 // Cases (combination of options): 200 // --strip-native-debug-symbols keep-debug-info:objcopy=</objcpy/path> 201 // --strip-native-debug-symbols keep-debug-info=ext:objcopy=</objcpy/path> 202 // --strip-native-debug-symbols omit-debug-info:objcopy=</objcpy/path> 203 String stripArg = config.get(STRIP_CMD_ARG); 204 if (stripArg != null && withChecks) { 205 validateStripArg(stripArg); 206 } 207 if (stripArg != null) { 208 stripBin = stripArg; 209 } 210 // Case (reversed combination) 211 // --strip-native-debug-symbols objcopy=</objcpy/path>:keep-debug-info=ext 212 // Note: cases like the following are not allowed by the parser 213 // --strip-native-debug-symbols objcopy=</objcpy/path>:keep-debug-info 214 // --strip-native-debug-symbols objcopy=</objcpy/path>:omit-debug-info 215 String keepDebugInfo = config.get(KEEP_DEBUG_INFO_ARG); 216 if (keepDebugInfo != null) { 217 hasKeepDebugInfo = true; 218 debuginfoExt = keepDebugInfo; 219 } 220 if (hasKeepDebugInfo && hasOmitDebugInfo) { 221 // cannot keep and omit debug info at the same time 222 throw new IllegalArgumentException( 223 PluginsResourceBundle.getMessage(resourceBundle, 224 NAME + ".iae.conflict", 225 NAME, 226 OMIT_DEBUG_INFO_ARG, 227 KEEP_DEBUG_INFO_ARG)); 228 } 229 includeDebugSymbols = hasKeepDebugInfo; 230 } 231 232 private StrippedDebugInfoBinary stripBinary(ResourcePoolEntry resource) { 233 Path tempDir = null; 234 try { 235 Path resPath = Paths.get(resource.path()); 236 String relativeFileName = resPath.getFileName().toString(); 237 tempDir = Files.createTempDirectory(NAME + relativeFileName); 238 Path resourceFileBinary = tempDir.resolve(relativeFileName); 239 String relativeDbgFileName = relativeFileName + "." + debuginfoExt; 240 241 Files.write(resourceFileBinary, resource.contentBytes()); 242 StrippedDebugInfoBinary strippedBin = new StrippedDebugInfoBinary(); 243 Path resourceFileDebugSymbols; 244 if (includeDebugSymbols) { 245 resourceFileDebugSymbols = tempDir.resolve(Paths.get(relativeDbgFileName)); 246 strippedBin.debugSymbols = createDebugSymbolsFile(resourceFileBinary, 247 resourceFileDebugSymbols, 248 relativeDbgFileName); 249 } 250 if (!stripBinary(resourceFileBinary)) { 251 return null; 252 } 253 if (includeDebugSymbols && !addGnuDebugLink(tempDir, 254 relativeFileName, 255 relativeDbgFileName)) { 256 return null; 257 } 258 strippedBin.strippedBinary = Files.readAllBytes(resourceFileBinary); 259 return strippedBin; 260 } catch (IOException | InterruptedException e) { 261 throw new PluginException(e); 262 } finally { 263 if (tempDir != null) { 264 deleteDirRecursivelyIgnoreResult(tempDir); 265 } 266 } 267 } 268 269 /* 270 * Equivalent of 'objcopy -g binFile'. Returning true iff stripping of the binary 271 * succeeded. 272 */ 273 private boolean stripBinary(Path binFile) 274 throws InterruptedException, IOException { 275 String filePath = binFile.toFile().getAbsolutePath(); 276 List<String> stripCmdLine = buildCmdLine(STRIP_DEBUG_SYMS_OPT, 277 filePath); 278 ProcessBuilder builder = new ProcessBuilder(stripCmdLine); 279 Process stripProc = builder.start(); 280 int retval = stripProc.waitFor(); 281 return retval == 0; 282 } 283 284 private List<String> buildCmdLine(String ...opts) { 285 List<String> objCopyCmd = cmdBuilder.build(stripBin); 286 objCopyCmd.addAll(Arrays.asList(opts)); 287 return objCopyCmd; 288 } 289 290 private void deleteDirRecursivelyIgnoreResult(Path tempDir) { 291 try { 292 Files.walkFileTree(tempDir, new SimpleFileVisitor<Path>() { 293 @Override 294 public FileVisitResult visitFile(Path file, 295 BasicFileAttributes attrs) throws IOException { 296 Files.delete(file); 297 return FileVisitResult.CONTINUE; 298 } 299 300 @Override 301 public FileVisitResult postVisitDirectory(Path dir, 302 IOException exc) throws IOException { 303 Files.delete(dir); 304 return FileVisitResult.CONTINUE; 305 } 306 }); 307 } catch (IOException e) { 308 // ignore deleting the temp dir 309 } 310 } 311 312 /* 313 * Equivalent of 'objcopy --add-gnu-debuglink=relativeDbgFileName binFile'. 314 * Returning true iff adding the debug link succeeded. 315 */ 316 private boolean addGnuDebugLink(Path currDir, 317 String binFile, 318 String relativeDbgFileName) 319 throws InterruptedException, IOException { 320 List<String> stripCmdLine = buildCmdLine(ADD_DEBUG_LINK_OPT + 321 "=" + relativeDbgFileName, 322 binFile); 323 ProcessBuilder builder = new ProcessBuilder(stripCmdLine); 324 builder.directory(currDir.toFile()); 325 Process stripProc = builder.start(); 326 int retval = stripProc.waitFor(); 327 return retval == 0; 328 329 } 330 331 /* 332 * Equivalent of 'objcopy --only-keep-debug binPath debugPath'. 333 * Returning the bytes of the file containing debug symbols. 334 */ 335 private byte[] createDebugSymbolsFile(Path binPath, 336 Path debugPath, 337 String dbgFileName) throws InterruptedException, 338 IOException { 339 String filePath = binPath.toFile().getAbsolutePath(); 340 String dbgPath = debugPath.toFile().getAbsolutePath(); 341 List<String> stripCmdLine = buildCmdLine(ONLY_KEEP_DEBUG_SYMS_OPT, 342 filePath, 343 dbgPath); 344 ProcessBuilder builder = new ProcessBuilder(stripCmdLine); 345 Process stripProc = builder.start(); 346 int retval = stripProc.waitFor(); 347 if (retval != 0) { 348 return null; 349 } else { 350 return Files.readAllBytes(debugPath); 351 } 352 } 353 354 private void validateStripArg(String stripArg) throws IllegalArgumentException { 355 try { 356 Path strip = Paths.get(stripArg); // verify it's a resonable path 357 if (!Files.isExecutable(strip)) { 358 throw new IllegalArgumentException( 359 PluginsResourceBundle.getMessage(resourceBundle, 360 NAME + ".invalidstrip", 361 stripArg)); 362 } 363 } catch (InvalidPathException e) { 364 throw new IllegalArgumentException( 365 PluginsResourceBundle.getMessage(resourceBundle, 366 NAME + ".invalidstrip", 367 e.getInput())); 368 } 369 } 370 371 private static class StrippedDebugInfoBinary { 372 byte[] strippedBinary; 373 byte[] debugSymbols; 374 } 375 376 // For better testing using mocked objcopy 377 public static interface ObjCopyCmdBuilder { 378 List<String> build(String objCopy); 379 } 380 381 private static final class DefaultObjCopyCmdBuilder implements ObjCopyCmdBuilder { 382 383 @Override 384 public List<String> build(String objCopy) { 385 List<String> cmdList = new ArrayList<>(); 386 cmdList.add(objCopy); 387 return cmdList; 388 } 389 390 } 391 392 }