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 }