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 import java.io.BufferedWriter; 26 import java.io.File; 27 import java.io.IOException; 28 import java.io.InputStream; 29 import java.io.PrintWriter; 30 import java.io.StringWriter; 31 import java.nio.file.FileVisitResult; 32 import java.nio.file.Files; 33 import java.nio.file.NoSuchFileException; 34 import java.nio.file.Path; 35 import java.nio.file.Paths; 36 import java.nio.file.SimpleFileVisitor; 37 import java.nio.file.attribute.BasicFileAttributes; 38 import java.util.ArrayList; 39 import java.util.Arrays; 40 import java.util.List; 41 import java.util.Scanner; 42 import java.util.spi.ToolProvider; 43 import java.util.stream.Collectors; 44 import java.util.stream.Stream; 45 46 import jdk.test.lib.compiler.CompilerUtils; 47 48 /* 49 * @test 50 * @requires os.family == "linux" 51 * @bug 8214796 52 * @summary Test --strip-native-debug-symbols plugin 53 * @author Severin Gehwolf 54 * @library /test/lib 55 * @modules jdk.compiler 56 * jdk.jlink 57 * @build jdk.test.lib.compiler.CompilerUtils 58 * @run main/othervm -Xmx1g StripNativeDebugSymbolsPluginTest 59 */ 60 public class StripNativeDebugSymbolsPluginTest { 61 62 private static final String OBJCOPY = "objcopy"; 63 private static final String PLUGIN_NAME = "strip-native-debug-symbols"; 64 private static final String MODULE_NAME_WITH_NATIVE = "fib"; 65 private static final String JAVA_HOME = System.getProperty("java.home"); 66 private static final String NATIVE_LIB_NAME = "libFib.so"; 67 private static final Path JAVA_LIB_PATH = Paths.get(System.getProperty("java.library.path")); 68 private static final Path LIB_FIB_SRC = JAVA_LIB_PATH.resolve(NATIVE_LIB_NAME); 69 private static final String FIBJNI_CLASS_NAME = "FibJNI.java"; 70 private static final Path JAVA_SRC_DIR = Paths.get(System.getProperty("test.src")) 71 .resolve("src") 72 .resolve(MODULE_NAME_WITH_NATIVE); 73 private static final Path FIBJNI_JAVA_CLASS = JAVA_SRC_DIR.resolve(FIBJNI_CLASS_NAME); 74 private static final String DEBUG_EXTENSION = "debug"; 75 private static final long ORIG_LIB_FIB_SIZE = LIB_FIB_SRC.toFile().length(); 76 77 public void testPluginLoaded() { 78 List<String> output = 79 JLink.run("--list-plugins").output(); 80 if (output.stream().anyMatch(s -> s.contains(PLUGIN_NAME))) { 81 System.out.println("DEBUG: " + PLUGIN_NAME + " plugin loaded as expected."); 82 } else { 83 throw new AssertionError("strip-native-debug-symbols plugin not in " + 84 "--list-plugins output."); 85 } 86 } 87 88 public void testStripNativeLibraryDefaults() throws Exception { 89 if (!hasJmods()) return; 90 91 Path libFibJmod = createLibFibJmod(); 92 93 Path imageDir = Paths.get("stripped-native-libs"); 94 JLink.run("--output", imageDir.toString(), 95 "--verbose", 96 "--module-path", modulePathWith(libFibJmod), 97 "--add-modules", MODULE_NAME_WITH_NATIVE, 98 "--strip-native-debug-symbols=defaults").output(); 99 Path libDir = imageDir.resolve("lib"); 100 Path postStripLib = libDir.resolve(NATIVE_LIB_NAME); 101 long postStripSize = postStripLib.toFile().length(); 102 103 if (postStripSize == 0) { 104 throw new AssertionError("Lib file size 0. Test error?!"); 105 } 106 // Heuristic: libLib.so is smaller post debug info stripping 107 if (postStripSize >= ORIG_LIB_FIB_SIZE) { 108 throw new AssertionError("Expected native library stripping to " + 109 "reduce file size. Expected < " + 110 ORIG_LIB_FIB_SIZE + ", got: " + postStripSize); 111 } else { 112 System.out.println("DEBUG: File size of " + postStripLib.toString() + 113 " " + postStripSize + " < " + ORIG_LIB_FIB_SIZE + " as expected." ); 114 } 115 verifyFibModule(imageDir); // Sanity check fib module which got libFib.so stripped 116 System.out.println("DEBUG: testStripNativeLibraryDefaults() PASSED!"); 117 } 118 119 public void testOptionsInvalidObjcopy() throws Exception { 120 if (!hasJmods()) return; 121 122 Path libFibJmod = createLibFibJmod(); 123 124 String notExists = "/do/not/exist/objcopy"; 125 126 Path imageDir = Paths.get("invalid-objcopy-command"); 127 String[] jlinkCmdArray = new String[] { 128 JAVA_HOME + File.separator + "bin" + File.separator + "jlink", 129 "--output", imageDir.toString(), 130 "--verbose", 131 "--module-path", modulePathWith(libFibJmod), 132 "--add-modules", MODULE_NAME_WITH_NATIVE, 133 "--strip-native-debug-symbols=options:objcopy-cmd=" + notExists, 134 }; 135 List<String> jlinkCmd = Arrays.asList(jlinkCmdArray); 136 System.out.println("Debug: command: " + jlinkCmd.stream().collect( 137 Collectors.joining(" "))); 138 ProcessBuilder builder = new ProcessBuilder(jlinkCmd); 139 Process p = builder.start(); 140 int status = p.waitFor(); 141 if (status == 0) { 142 throw new AssertionError("Expected jlink to fail!"); 143 } else { 144 verifyInvalidObjcopyError(p.getInputStream(), notExists); 145 System.out.println("DEBUG: testOptionsInvalidObjcopy() PASSED!"); 146 } 147 } 148 149 public void testStripNativeLibsDebugSymsIncluded() throws Exception { 150 if (!hasJmods()) return; 151 152 Path libFibJmod = createLibFibJmod(); 153 154 Path imageDir = Paths.get("stripped-native-libs-with-debug"); 155 JLink.run("--output", imageDir.toString(), 156 "--verbose", 157 "--module-path", modulePathWith(libFibJmod), 158 "--add-modules", MODULE_NAME_WITH_NATIVE, 159 "--strip-native-debug-symbols=options:include-debug-syms=true:" + 160 "debuginfo-file-ext=" + DEBUG_EXTENSION); 161 162 Path libDir = imageDir.resolve("lib"); 163 Path postStripLib = libDir.resolve(NATIVE_LIB_NAME); 164 long postStripSize = postStripLib.toFile().length(); 165 166 if (postStripSize == 0) { 167 throw new AssertionError("Lib file size 0. Test error?!"); 168 } 169 // Heuristic: libLib.so is smaller post debug info stripping 170 if (postStripSize >= ORIG_LIB_FIB_SIZE) { 171 throw new AssertionError("Expected native library stripping to " + 172 "reduce file size. Expected < " + 173 ORIG_LIB_FIB_SIZE + ", got: " + postStripSize); 174 } else { 175 System.out.println("DEBUG: File size of " + postStripLib.toString() + 176 " " + postStripSize + " < " + ORIG_LIB_FIB_SIZE + " as expected." ); 177 } 178 // stripped with option to preserve debug symbols file 179 verifyDebugInfoSymbolFilePresent(imageDir); 180 System.out.println("DEBUG: testStripNativeLibsDebugSymsIncluded() PASSED!"); 181 } 182 183 // Create the jmod with the native library 184 private Path createLibFibJmod() throws IOException { 185 JmodFileBuilder jmodBuilder = new JmodFileBuilder(MODULE_NAME_WITH_NATIVE); 186 jmodBuilder.javaClass(FIBJNI_JAVA_CLASS); 187 jmodBuilder.nativeLib(LIB_FIB_SRC); 188 return jmodBuilder.build(); 189 } 190 191 private String modulePathWith(Path jmod) { 192 return Paths.get(JAVA_HOME, "jmods").toString() + 193 File.pathSeparator + jmod.getParent().toString(); 194 } 195 196 private boolean hasJmods() { 197 if (!Files.exists(Paths.get(JAVA_HOME, "jmods"))) { 198 System.err.println("Test skipped. NO jmods directory"); 199 return false; 200 } 201 return true; 202 } 203 204 private void verifyInvalidObjcopyError(InputStream errInput, String match) { 205 boolean foundMatch = false; 206 try (Scanner scanner = new Scanner(errInput)) { 207 while (scanner.hasNextLine()) { 208 String line = scanner.nextLine(); 209 System.out.println("DEBUG: >>>> " + line); 210 if (line.contains(match)) { 211 foundMatch = true; 212 break; 213 } 214 } 215 } 216 if (!foundMatch) { 217 throw new AssertionError("Expected to find " + match + 218 " in error stream."); 219 } else { 220 System.out.println("DEBUG: Found string " + match + " as expected."); 221 } 222 } 223 224 private void verifyDebugInfoSymbolFilePresent(Path image) 225 throws IOException, InterruptedException { 226 Path debugSymsFile = image.resolve("lib/libFib.so.debug"); 227 if (!Files.exists(debugSymsFile)) { 228 throw new AssertionError("Expected stripped debug info file " + 229 debugSymsFile.toString() + " to exist."); 230 } 231 long debugSymsSize = debugSymsFile.toFile().length(); 232 if (debugSymsSize <= 0) { 233 throw new AssertionError("sanity check for fib.FibJNI failed " + 234 "post-stripping!"); 235 } else { 236 System.out.println("DEBUG: Debug symbols stripped from libFib.so " + 237 "present (" + debugSymsFile.toString() + ") as expected."); 238 } 239 } 240 241 private void verifyFibModule(Path image) 242 throws IOException, InterruptedException { 243 System.out.println("DEBUG: sanity checking fib module..."); 244 Path launcher = image.resolve("bin/java"); 245 List<String> args = new ArrayList<>(); 246 args.add(launcher.toString()); 247 args.add("--add-modules"); 248 args.add(MODULE_NAME_WITH_NATIVE); 249 args.add("fib.FibJNI"); 250 args.add("7"); 251 args.add("13"); // fib(7) == 13 252 System.out.println("DEBUG: [command] " + 253 args.stream().collect(Collectors.joining(" "))); 254 Process proc = new ProcessBuilder(args).inheritIO().start(); 255 int status = proc.waitFor(); 256 if (status == 0) { 257 System.out.println("DEBUG: sanity checking fib module... PASSED!"); 258 } else { 259 throw new AssertionError("sanity check for fib.FibJNI failed post-" + 260 "stripping!"); 261 } 262 } 263 264 public static void main(String[] args) throws Exception { 265 if (!isObjcopyPresent()) { 266 System.out.println("Test skipped. Requires objcopy to be installed."); 267 return; 268 } 269 StripNativeDebugSymbolsPluginTest test = new StripNativeDebugSymbolsPluginTest(); 270 test.testPluginLoaded(); 271 test.testStripNativeLibraryDefaults(); 272 test.testStripNativeLibsDebugSymsIncluded(); 273 test.testOptionsInvalidObjcopy(); 274 } 275 276 private static boolean isObjcopyPresent() throws Exception { 277 String[] objcopyVersion = new String[] { 278 OBJCOPY, "--version", 279 }; 280 List<String> command = Arrays.asList(objcopyVersion); 281 try { 282 ProcessBuilder builder = new ProcessBuilder(command); 283 builder.inheritIO(); 284 Process p = builder.start(); 285 int status = p.waitFor(); 286 if (status != 0) { 287 System.out.println("Debug: objcopy binary doesn't seem to be " + 288 "present or functional."); 289 return false; 290 } 291 } catch (IOException e) { 292 System.out.println("Debug: objcopy binary doesn't seem to be present " + 293 "or functional."); 294 return false; 295 } 296 return true; 297 } 298 299 static class JLink { 300 static final ToolProvider JLINK_TOOL = ToolProvider.findFirst("jlink") 301 .orElseThrow(() -> 302 new RuntimeException("jlink tool not found") 303 ); 304 305 static JLink run(String... options) { 306 JLink jlink = new JLink(); 307 if (jlink.execute(options) != 0) { 308 throw new AssertionError("Jlink expected to exit with 0 return code"); 309 } 310 return jlink; 311 } 312 313 final List<String> output = new ArrayList<>(); 314 private int execute(String... options) { 315 System.out.println("jlink " + 316 Stream.of(options).collect(Collectors.joining(" "))); 317 318 StringWriter writer = new StringWriter(); 319 PrintWriter pw = new PrintWriter(writer); 320 int rc = JLINK_TOOL.run(pw, pw, options); 321 System.out.println(writer.toString()); 322 Stream.of(writer.toString().split("\\v")) 323 .map(String::trim) 324 .forEach(output::add); 325 return rc; 326 } 327 328 boolean contains(String s) { 329 return output.contains(s); 330 } 331 332 List<String> output() { 333 return output; 334 } 335 } 336 337 /** 338 * Builder to create JMOD file 339 */ 340 private static class JmodFileBuilder { 341 342 private static final ToolProvider JMOD_TOOL = ToolProvider 343 .findFirst("jmod") 344 .orElseThrow(() -> 345 new RuntimeException("jmod tool not found") 346 ); 347 private static final Path SRC_DIR = Paths.get("src"); 348 private static final Path MODS_DIR = Paths.get("mod"); 349 private static final Path JMODS_DIR = Paths.get("jmods"); 350 private static final Path LIBS_DIR = Paths.get("libs"); 351 352 private final String name; 353 private final List<Path> nativeLibs = new ArrayList<>(); 354 private final List<Path> javaClasses = new ArrayList<>(); 355 356 private JmodFileBuilder(String name) throws IOException { 357 this.name = name; 358 359 deleteDirectory(MODS_DIR); 360 deleteDirectory(SRC_DIR); 361 deleteDirectory(LIBS_DIR); 362 deleteDirectory(JMODS_DIR); 363 Path msrc = SRC_DIR.resolve(name); 364 if (Files.exists(msrc)) { 365 deleteDirectory(msrc); 366 } 367 } 368 369 JmodFileBuilder nativeLib(Path libFileSrc) { 370 nativeLibs.add(libFileSrc); 371 return this; 372 } 373 374 JmodFileBuilder javaClass(Path srcPath) { 375 javaClasses.add(srcPath); 376 return this; 377 } 378 379 Path build() throws IOException { 380 compileModule(); 381 return createJmodFile(); 382 } 383 384 private void compileModule() throws IOException { 385 Path msrc = SRC_DIR.resolve(name); 386 Files.createDirectories(msrc); 387 // copy class using native lib to expected path 388 if (javaClasses.size() > 0) { 389 for (Path srcPath: javaClasses) { 390 Path targetPath = msrc.resolve(srcPath.getFileName()); 391 Files.copy(srcPath, targetPath); 392 } 393 } 394 // generate module-info file. 395 Path minfo = msrc.resolve("module-info.java"); 396 try (BufferedWriter bw = Files.newBufferedWriter(minfo); 397 PrintWriter writer = new PrintWriter(bw)) { 398 writer.format("module %s { }%n", name); 399 } 400 401 if (!CompilerUtils.compile(msrc, MODS_DIR, 402 "--module-source-path", 403 SRC_DIR.toString())) { 404 405 } 406 } 407 408 private Path createJmodFile() throws IOException { 409 Path mclasses = MODS_DIR.resolve(name); 410 Files.createDirectories(JMODS_DIR); 411 Path outfile = JMODS_DIR.resolve(name + ".jmod"); 412 List<String> args = new ArrayList<>(); 413 args.add("create"); 414 // add classes 415 args.add("--class-path"); 416 args.add(mclasses.toString()); 417 // native libs 418 if (nativeLibs.size() > 0) { 419 // Copy the JNI library to the expected path 420 Files.createDirectories(LIBS_DIR); 421 for (Path srcLib: nativeLibs) { 422 Path targetLib = LIBS_DIR.resolve(srcLib.getFileName()); 423 Files.copy(srcLib, targetLib); 424 } 425 args.add("--libs"); 426 args.add(LIBS_DIR.toString()); 427 } 428 args.add(outfile.toString()); 429 430 if (Files.exists(outfile)) { 431 Files.delete(outfile); 432 } 433 434 System.out.println("jmod " + 435 args.stream().collect(Collectors.joining(" "))); 436 437 int rc = JMOD_TOOL.run(System.out, System.out, 438 args.toArray(new String[args.size()])); 439 if (rc != 0) { 440 throw new AssertionError("jmod failed: rc = " + rc); 441 } 442 return outfile; 443 } 444 445 private static void deleteDirectory(Path dir) throws IOException { 446 try { 447 Files.walkFileTree(dir, new SimpleFileVisitor<Path>() { 448 @Override 449 public FileVisitResult visitFile(Path file, 450 BasicFileAttributes attrs) 451 throws IOException 452 { 453 Files.delete(file); 454 return FileVisitResult.CONTINUE; 455 } 456 457 @Override 458 public FileVisitResult postVisitDirectory(Path dir, 459 IOException exc) 460 throws IOException 461 { 462 Files.delete(dir); 463 return FileVisitResult.CONTINUE; 464 } 465 }); 466 } catch (NoSuchFileException e) { 467 // ignore non-existing files 468 } 469 } 470 } 471 }