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(Collectors.joining(" "))); 137 ProcessBuilder builder = new ProcessBuilder(jlinkCmd); 138 Process p = builder.start(); 139 int status = p.waitFor(); 140 if (status == 0) { 141 throw new AssertionError("Expected jlink to fail!"); 142 } else { 143 verifyInvalidObjcopyError(p.getInputStream(), notExists); 144 System.out.println("DEBUG: testOptionsInvalidObjcopy() PASSED!"); 145 } 146 } 147 148 public void testStripNativeLibsDebugSymsIncluded() throws Exception { 149 if (!hasJmods()) return; 150 151 Path libFibJmod = createLibFibJmod(); 152 153 Path imageDir = Paths.get("stripped-native-libs-with-debug"); 154 JLink.run("--output", imageDir.toString(), 155 "--verbose", 156 "--module-path", modulePathWith(libFibJmod), 157 "--add-modules", MODULE_NAME_WITH_NATIVE, 158 "--strip-native-debug-symbols=options:include-debug-syms=true:debuginfo-file-ext=" + DEBUG_EXTENSION); 159 160 Path libDir = imageDir.resolve("lib"); 161 Path postStripLib = libDir.resolve(NATIVE_LIB_NAME); 162 long postStripSize = postStripLib.toFile().length(); 163 164 if (postStripSize == 0) { 165 throw new AssertionError("Lib file size 0. Test error?!"); 166 } 167 // Heuristic: libLib.so is smaller post debug info stripping 168 if (postStripSize >= ORIG_LIB_FIB_SIZE) { 169 throw new AssertionError("Expected native library stripping to " + 170 "reduce file size. Expected < " + 171 ORIG_LIB_FIB_SIZE + ", got: " + postStripSize); 172 } else { 173 System.out.println("DEBUG: File size of " + postStripLib.toString() + 174 " " + postStripSize + " < " + ORIG_LIB_FIB_SIZE + " as expected." ); 175 } 176 // stripped with option to preserve debug symbols file 177 verifyDebugInfoSymbolFilePresent(imageDir); 178 System.out.println("DEBUG: testStripNativeLibsDebugSymsIncluded() PASSED!"); 179 } 180 181 // Create the jmod with the native library 182 private Path createLibFibJmod() throws IOException { 183 JmodFileBuilder jmodBuilder = new JmodFileBuilder(MODULE_NAME_WITH_NATIVE); 184 jmodBuilder.javaClass(FIBJNI_JAVA_CLASS); 185 jmodBuilder.nativeLib(LIB_FIB_SRC); 186 return jmodBuilder.build(); 187 } 188 189 private String modulePathWith(Path jmod) { 190 return Paths.get(JAVA_HOME, "jmods").toString() + 191 File.pathSeparator + jmod.getParent().toString(); 192 } 193 194 private boolean hasJmods() { 195 if (!Files.exists(Paths.get(JAVA_HOME, "jmods"))) { 196 System.err.println("Test skipped. NO jmods directory"); 197 return false; 198 } 199 return true; 200 } 201 202 private void verifyInvalidObjcopyError(InputStream errInput, String match) { 203 boolean foundMatch = false; 204 try (Scanner scanner = new Scanner(errInput)) { 205 while (scanner.hasNextLine()) { 206 String line = scanner.nextLine(); 207 System.out.println("DEBUG: >>>> " + line); 208 if (line.contains(match)) { 209 foundMatch = true; 210 break; 211 } 212 } 213 } 214 if (!foundMatch) { 215 throw new AssertionError("Expected to find " + match + 216 " in error stream."); 217 } else { 218 System.out.println("DEBUG: Found string " + match + " as expected."); 219 } 220 } 221 222 private void verifyDebugInfoSymbolFilePresent(Path image) throws IOException, InterruptedException { 223 Path debugSymsFile = image.resolve("lib/libFib.so.debug"); 224 if (!Files.exists(debugSymsFile)) { 225 throw new AssertionError("Expected stripped debug info file " + debugSymsFile.toString() + " to exist."); 226 } 227 long debugSymsSize = debugSymsFile.toFile().length(); 228 if (debugSymsSize <= 0) { 229 throw new AssertionError("sanity check for fib.FibJNI failed post-stripping!"); 230 } else { 231 System.out.println("DEBUG: Debug symbols stripped from libFib.so present (" + debugSymsFile.toString() + ") as expected."); 232 } 233 } 234 235 private void verifyFibModule(Path image) throws IOException, InterruptedException { 236 System.out.println("DEBUG: sanity checking fib module..."); 237 Path launcher = image.resolve("bin/java"); 238 List<String> args = new ArrayList<>(); 239 args.add(launcher.toString()); 240 args.add("--add-modules"); 241 args.add(MODULE_NAME_WITH_NATIVE); 242 args.add("fib.FibJNI"); 243 args.add("7"); 244 args.add("13"); // fib(7) == 13 245 System.out.println("DEBUG: [command] " + args.stream().collect(Collectors.joining(" "))); 246 Process proc = new ProcessBuilder(args).inheritIO().start(); 247 int status = proc.waitFor(); 248 if (status == 0) { 249 System.out.println("DEBUG: sanity checking fib module... PASSED!"); 250 } else { 251 throw new AssertionError("sanity check for fib.FibJNI failed post-stripping!"); 252 } 253 } 254 255 public static void main(String[] args) throws Exception { 256 if (!isObjcopyPresent()) { 257 System.out.println("Test skipped. Requires objcopy to be installed."); 258 return; 259 } 260 StripNativeDebugSymbolsPluginTest test = new StripNativeDebugSymbolsPluginTest(); 261 test.testPluginLoaded(); 262 test.testStripNativeLibraryDefaults(); 263 test.testStripNativeLibsDebugSymsIncluded(); 264 test.testOptionsInvalidObjcopy(); 265 } 266 267 private static boolean isObjcopyPresent() throws Exception { 268 String[] objcopyVersion = new String[] { 269 OBJCOPY, "--version", 270 }; 271 try { 272 ProcessBuilder builder = new ProcessBuilder(Arrays.asList(objcopyVersion)); 273 builder.inheritIO(); 274 Process p = builder.start(); 275 int status = p.waitFor(); 276 if (status != 0) { 277 System.out.println("Debug: objcopy binary doesn't seem to be present or functional."); 278 return false; 279 } 280 } catch (IOException e) { 281 System.out.println("Debug: objcopy binary doesn't seem to be present or functional."); 282 return false; 283 } 284 return true; 285 } 286 287 static class JLink { 288 static final ToolProvider JLINK_TOOL = ToolProvider.findFirst("jlink") 289 .orElseThrow(() -> 290 new RuntimeException("jlink tool not found") 291 ); 292 293 static JLink run(String... options) { 294 JLink jlink = new JLink(); 295 if (jlink.execute(options) != 0) { 296 throw new AssertionError("Jlink expected to exit with 0 return code"); 297 } 298 return jlink; 299 } 300 301 final List<String> output = new ArrayList<>(); 302 private int execute(String... options) { 303 System.out.println("jlink " + 304 Stream.of(options).collect(Collectors.joining(" "))); 305 306 StringWriter writer = new StringWriter(); 307 PrintWriter pw = new PrintWriter(writer); 308 int rc = JLINK_TOOL.run(pw, pw, options); 309 System.out.println(writer.toString()); 310 Stream.of(writer.toString().split("\\v")) 311 .map(String::trim) 312 .forEach(output::add); 313 return rc; 314 } 315 316 boolean contains(String s) { 317 return output.contains(s); 318 } 319 320 List<String> output() { 321 return output; 322 } 323 } 324 325 /** 326 * Builder to create JMOD file 327 */ 328 private static class JmodFileBuilder { 329 330 private static final ToolProvider JMOD_TOOL = ToolProvider.findFirst("jmod") 331 .orElseThrow(() -> 332 new RuntimeException("jmod tool not found") 333 ); 334 private static final Path SRC_DIR = Paths.get("src"); 335 private static final Path MODS_DIR = Paths.get("mod"); 336 private static final Path JMODS_DIR = Paths.get("jmods"); 337 private static final Path LIBS_DIR = Paths.get("libs"); 338 339 private final String name; 340 private final List<Path> nativeLibs = new ArrayList<>(); 341 private final List<Path> javaClasses = new ArrayList<>(); 342 343 private JmodFileBuilder(String name) throws IOException { 344 this.name = name; 345 346 deleteDirectory(MODS_DIR); 347 deleteDirectory(SRC_DIR); 348 deleteDirectory(LIBS_DIR); 349 deleteDirectory(JMODS_DIR); 350 Path msrc = SRC_DIR.resolve(name); 351 if (Files.exists(msrc)) { 352 deleteDirectory(msrc); 353 } 354 } 355 356 JmodFileBuilder nativeLib(Path libFileSrc) { 357 nativeLibs.add(libFileSrc); 358 return this; 359 } 360 361 JmodFileBuilder javaClass(Path srcPath) { 362 javaClasses.add(srcPath); 363 return this; 364 } 365 366 Path build() throws IOException { 367 compileModule(); 368 return createJmodFile(); 369 } 370 371 private void compileModule() throws IOException { 372 Path msrc = SRC_DIR.resolve(name); 373 Files.createDirectories(msrc); 374 // copy class using native lib to expected path 375 if (javaClasses.size() > 0) { 376 for (Path srcPath: javaClasses) { 377 Path targetPath = msrc.resolve(srcPath.getFileName()); 378 Files.copy(srcPath, targetPath); 379 } 380 } 381 // generate module-info file. 382 Path minfo = msrc.resolve("module-info.java"); 383 try (BufferedWriter bw = Files.newBufferedWriter(minfo); 384 PrintWriter writer = new PrintWriter(bw)) { 385 writer.format("module %s { }%n", name); 386 } 387 388 if (!CompilerUtils.compile(msrc, MODS_DIR, 389 "--module-source-path", 390 SRC_DIR.toString())) { 391 392 } 393 } 394 395 private Path createJmodFile() throws IOException { 396 Path mclasses = MODS_DIR.resolve(name); 397 Files.createDirectories(JMODS_DIR); 398 Path outfile = JMODS_DIR.resolve(name + ".jmod"); 399 List<String> args = new ArrayList<>(); 400 args.add("create"); 401 // add classes 402 args.add("--class-path"); 403 args.add(mclasses.toString()); 404 // native libs 405 if (nativeLibs.size() > 0) { 406 // Copy the JNI library to the expected path 407 Files.createDirectories(LIBS_DIR); 408 for (Path srcLib: nativeLibs) { 409 Path targetLib = LIBS_DIR.resolve(srcLib.getFileName()); 410 Files.copy(srcLib, targetLib); 411 } 412 args.add("--libs"); 413 args.add(LIBS_DIR.toString()); 414 } 415 args.add(outfile.toString()); 416 417 if (Files.exists(outfile)) { 418 Files.delete(outfile); 419 } 420 421 System.out.println("jmod " + 422 args.stream().collect(Collectors.joining(" "))); 423 424 int rc = JMOD_TOOL.run(System.out, System.out, 425 args.toArray(new String[args.size()])); 426 if (rc != 0) { 427 throw new AssertionError("jmod failed: rc = " + rc); 428 } 429 return outfile; 430 } 431 432 private static void deleteDirectory(Path dir) throws IOException { 433 try { 434 Files.walkFileTree(dir, new SimpleFileVisitor<Path>() { 435 @Override 436 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 437 throws IOException 438 { 439 Files.delete(file); 440 return FileVisitResult.CONTINUE; 441 } 442 443 @Override 444 public FileVisitResult postVisitDirectory(Path dir, IOException exc) 445 throws IOException 446 { 447 Files.delete(dir); 448 return FileVisitResult.CONTINUE; 449 } 450 }); 451 } catch (NoSuchFileException e) { 452 // ignore non-existing files 453 } 454 } 455 } 456 }