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 }