8214796: Create a jlink plugin for stripping debug info symbols from native libraries
authorsgehwolf
Thu, 14 Mar 2019 14:04:39 +0100
changeset 54824 adb3a3aa2e52
parent 54823 1b940da275d2
child 54825 1b03400e5a8f
8214796: Create a jlink plugin for stripping debug info symbols from native libraries Reviewed-by: alanb, mchung, erikj, ihse
make/common/TestFilesCompilation.gmk
make/gensrc/Gensrc-jdk.jlink.gmk
make/test/JtregNativeJdk.gmk
src/jdk.jlink/linux/classes/jdk/tools/jlink/internal/plugins/StripNativeDebugSymbolsPlugin.java
src/jdk.jlink/linux/classes/jdk/tools/jlink/resources/strip_native_debug_symbols_plugin.properties
src/jdk.jlink/linux/classes/module-info.java.extra
src/jdk.jlink/share/classes/jdk/tools/jlink/internal/TaskHelper.java
src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/DefaultStripDebugPlugin.java
src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/PluginsResourceBundle.java
src/jdk.jlink/share/classes/jdk/tools/jlink/resources/plugins.properties
test/jdk/tools/jlink/IntegrationTest.java
test/jdk/tools/jlink/plugins/DefaultStripDebugPluginTest.java
test/jdk/tools/jlink/plugins/StripNativeDebugSymbolsPlugin/FakeObjCopy.java
test/jdk/tools/jlink/plugins/StripNativeDebugSymbolsPlugin/StripNativeDebugSymbolsPluginTest.java
test/jdk/tools/jlink/plugins/StripNativeDebugSymbolsPlugin/src/fib/FibJNI.java
test/jdk/tools/jlink/plugins/StripNativeDebugSymbolsPlugin/src/libFib.c
--- a/make/common/TestFilesCompilation.gmk	Thu Apr 25 17:27:37 2019 +0100
+++ b/make/common/TestFilesCompilation.gmk	Thu Mar 14 14:04:39 2019 +0100
@@ -103,7 +103,7 @@
         TOOLCHAIN := $(if $$(filter %.cpp, $$(file)), TOOLCHAIN_LINK_CXX, TOOLCHAIN_DEFAULT), \
         OPTIMIZATION := $$(if $$($1_OPTIMIZATION_$$(name)),$$($1_OPTIMIZATION_$$(name)),LOW), \
         COPY_DEBUG_SYMBOLS := false, \
-        STRIP_SYMBOLS := false, \
+        STRIP_SYMBOLS := $$(if $$($1_STRIP_SYMBOLS_$$(name)),$$($1_STRIP_SYMBOLS_$$(name)),false), \
     )) \
     $$(eval $1 += $$(BUILD_TEST_$$(name)) )  \
   )
--- a/make/gensrc/Gensrc-jdk.jlink.gmk	Thu Apr 25 17:27:37 2019 +0100
+++ b/make/gensrc/Gensrc-jdk.jlink.gmk	Thu Mar 14 14:04:39 2019 +0100
@@ -24,13 +24,17 @@
 #
 
 include GensrcCommonJdk.gmk
+include GensrcProperties.gmk
+include Modules.gmk
 
 ################################################################################
 
-include GensrcProperties.gmk
+# Use wildcard so as to avoid getting non-existing directories back
+JLINK_RESOURCES_DIRS := $(wildcard $(addsuffix /jdk/tools/jlink/resources, \
+    $(call FindModuleSrcDirs, jdk.jlink)))
 
 $(eval $(call SetupCompileProperties, JLINK_PROPERTIES, \
-    SRC_DIRS := $(TOPDIR)/src/jdk.jlink/share/classes/jdk/tools/jlink/resources, \
+    SRC_DIRS := $(JLINK_RESOURCES_DIRS), \
     CLASS := ListResourceBundle, \
 ))
 
--- a/make/test/JtregNativeJdk.gmk	Thu Apr 25 17:27:37 2019 +0100
+++ b/make/test/JtregNativeJdk.gmk	Thu Mar 14 14:04:39 2019 +0100
@@ -84,6 +84,13 @@
   BUILD_JDK_JTREG_EXCLUDE += exeJniInvocationTest.c
 endif
 
+ifeq ($(call isTargetOs, linux), true)
+  # Unconditionally compile with debug symbols and don't ever perform
+  # stripping during the test libraries' build.
+  BUILD_JDK_JTREG_LIBRARIES_CFLAGS_libFib := -g
+  BUILD_JDK_JTREG_LIBRARIES_STRIP_SYMBOLS_libFib := false
+endif
+
 # This evaluation is expensive and should only be done if this target was
 # explicitly called.
 ifneq ($(filter build-test-jdk-jtreg-native, $(MAKECMDGOALS)), )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.jlink/linux/classes/jdk/tools/jlink/internal/plugins/StripNativeDebugSymbolsPlugin.java	Thu Mar 14 14:04:39 2019 +0100
@@ -0,0 +1,488 @@
+/*
+ * Copyright (c) 2019, Red Hat, Inc.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package jdk.tools.jlink.internal.plugins;
+
+import java.io.IOException;
+import java.lang.ProcessBuilder.Redirect;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.MissingResourceException;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.ResourceBundle;
+
+import jdk.tools.jlink.plugin.Plugin;
+import jdk.tools.jlink.plugin.PluginException;
+import jdk.tools.jlink.plugin.ResourcePool;
+import jdk.tools.jlink.plugin.ResourcePoolBuilder;
+import jdk.tools.jlink.plugin.ResourcePoolEntry;
+
+/**
+ * Platform specific jlink plugin for stripping debug symbols from native
+ * libraries and binaries.
+ *
+ */
+public final class StripNativeDebugSymbolsPlugin implements Plugin {
+
+    public static final String NAME = "strip-native-debug-symbols";
+    private static final boolean DEBUG = Boolean.getBoolean("jlink.debug");
+    private static final String DEFAULT_STRIP_CMD = "objcopy";
+    private static final String STRIP_CMD_ARG = DEFAULT_STRIP_CMD;
+    private static final String KEEP_DEBUG_INFO_ARG = "keep-debuginfo-files";
+    private static final String EXCLUDE_DEBUG_INFO_ARG = "exclude-debuginfo-files";
+    private static final String DEFAULT_DEBUG_EXT = "debuginfo";
+    private static final String STRIP_DEBUG_SYMS_OPT = "-g";
+    private static final String ONLY_KEEP_DEBUG_SYMS_OPT = "--only-keep-debug";
+    private static final String ADD_DEBUG_LINK_OPT = "--add-gnu-debuglink";
+    private static final ResourceBundle resourceBundle;
+    private static final String SHARED_LIBS_EXT = ".so"; // for Linux/Unix
+
+    static {
+        Locale locale = Locale.getDefault();
+        try {
+            resourceBundle = ResourceBundle.getBundle("jdk.tools.jlink."
+                    + "resources.strip_native_debug_symbols_plugin", locale);
+        } catch (MissingResourceException e) {
+            throw new InternalError("Cannot find jlink plugin resource bundle (" +
+                        NAME + ") for locale " + locale);
+        }
+    }
+
+    private final ObjCopyCmdBuilder cmdBuilder;
+    private boolean includeDebugSymbols;
+    private String stripBin;
+    private String debuginfoExt;
+
+    public StripNativeDebugSymbolsPlugin() {
+        this(new DefaultObjCopyCmdBuilder());
+    }
+
+    public StripNativeDebugSymbolsPlugin(ObjCopyCmdBuilder cmdBuilder) {
+        this.cmdBuilder = cmdBuilder;
+    }
+
+    @Override
+    public String getName() {
+        return NAME;
+    }
+
+    @Override
+    public ResourcePool transform(ResourcePool in, ResourcePoolBuilder out) {
+        StrippedDebugInfoBinaryBuilder builder = new StrippedDebugInfoBinaryBuilder(
+                                                        includeDebugSymbols,
+                                                        debuginfoExt,
+                                                        cmdBuilder,
+                                                        stripBin);
+        in.transformAndCopy((resource) -> {
+            ResourcePoolEntry res = resource;
+            if ((resource.type() == ResourcePoolEntry.Type.NATIVE_LIB &&
+                 resource.path().endsWith(SHARED_LIBS_EXT)) ||
+                resource.type() == ResourcePoolEntry.Type.NATIVE_CMD) {
+                Optional<StrippedDebugInfoBinary> strippedBin = builder.build(resource);
+                if (strippedBin.isPresent()) {
+                    StrippedDebugInfoBinary sb = strippedBin.get();
+                    res = sb.strippedBinary();
+                    if (includeDebugSymbols) {
+                        Optional<ResourcePoolEntry> debugInfo = sb.debugSymbols();
+                        if (debugInfo.isEmpty()) {
+                            String key = NAME + ".error.debugfile";
+                            logError(resource, key);
+                        } else {
+                            out.add(debugInfo.get());
+                        }
+                    }
+                } else {
+                    String key = NAME + ".error.file";
+                    logError(resource, key);
+                }
+            }
+            return res;
+        }, out);
+
+        return out.build();
+    }
+
+    private void logError(ResourcePoolEntry resource, String msgKey) {
+        String msg = PluginsResourceBundle.getMessage(resourceBundle,
+                                                      msgKey,
+                                                      NAME,
+                                                      resource.path());
+        System.err.println(msg);
+    }
+
+    @Override
+    public Category getType() {
+        return Category.TRANSFORMER;
+    }
+
+    @Override
+    public String getDescription() {
+        String key = NAME + ".description";
+        return PluginsResourceBundle.getMessage(resourceBundle, key);
+    }
+
+    @Override
+    public boolean hasArguments() {
+        return true;
+    }
+
+    @Override
+    public String getArgumentsDescription() {
+        String key = NAME + ".argument";
+        return PluginsResourceBundle.getMessage(resourceBundle, key);
+    }
+
+    @Override
+    public void configure(Map<String, String> config) {
+        doConfigure(true, config);
+    }
+
+    // For testing so that validation can be turned off
+    public void doConfigure(boolean withChecks, Map<String, String> orig) {
+        Map<String, String> config = new HashMap<>(orig);
+        String arg = config.remove(NAME);
+
+        stripBin = DEFAULT_STRIP_CMD;
+        debuginfoExt = DEFAULT_DEBUG_EXT;
+
+        // argument must never be null as it requires at least one
+        // argument, since hasArguments() == true. This might change once
+        // 8218761 is implemented.
+        if (arg == null) {
+            throw new InternalError();
+        }
+        boolean hasOmitDebugInfo = false;
+        boolean hasKeepDebugInfo = false;
+
+        if (KEEP_DEBUG_INFO_ARG.equals(arg)) {
+            // Case: --strip-native-debug-symbols keep-debuginfo-files
+            hasKeepDebugInfo = true;
+        } else if (arg.startsWith(KEEP_DEBUG_INFO_ARG)) {
+            // Case: --strip-native-debug-symbols keep-debuginfo-files=foo
+            String[] tokens = arg.split("=");
+            if (tokens.length != 2 || !KEEP_DEBUG_INFO_ARG.equals(tokens[0])) {
+                throw new IllegalArgumentException(
+                        PluginsResourceBundle.getMessage(resourceBundle,
+                                                         NAME + ".iae", NAME, arg));
+            }
+            hasKeepDebugInfo = true;
+            debuginfoExt = tokens[1];
+        }
+        if (EXCLUDE_DEBUG_INFO_ARG.equals(arg) || arg.startsWith(EXCLUDE_DEBUG_INFO_ARG + "=")) {
+            // Case: --strip-native-debug-symbols exclude-debuginfo-files[=something]
+            hasOmitDebugInfo = true;
+        }
+        if (arg.startsWith(STRIP_CMD_ARG)) {
+            // Case: --strip-native-debug-symbols objcopy=<path/to/objcopy
+            String[] tokens = arg.split("=");
+            if (tokens.length != 2 || !STRIP_CMD_ARG.equals(tokens[0])) {
+                throw new IllegalArgumentException(
+                        PluginsResourceBundle.getMessage(resourceBundle,
+                                                         NAME + ".iae", NAME, arg));
+            }
+            if (withChecks) {
+                validateStripArg(tokens[1]);
+            }
+            stripBin = tokens[1];
+        }
+        // Cases (combination of options):
+        //   --strip-native-debug-symbols keep-debuginfo-files:objcopy=</objcpy/path>
+        //   --strip-native-debug-symbols keep-debuginfo-files=ext:objcopy=</objcpy/path>
+        //   --strip-native-debug-symbols exclude-debuginfo-files:objcopy=</objcpy/path>
+        String stripArg = config.remove(STRIP_CMD_ARG);
+        if (stripArg != null && withChecks) {
+            validateStripArg(stripArg);
+        }
+        if (stripArg != null) {
+            stripBin = stripArg;
+        }
+        // Case (reversed combination)
+        //   --strip-native-debug-symbols objcopy=</objcpy/path>:keep-debuginfo-files=ext
+        // Note: cases like the following are not allowed by the parser
+        //   --strip-native-debug-symbols objcopy=</objcpy/path>:keep-debuginfo-files
+        //   --strip-native-debug-symbols objcopy=</objcpy/path>:exclude-debuginfo-files
+        String keepDebugInfo = config.remove(KEEP_DEBUG_INFO_ARG);
+        if (keepDebugInfo != null) {
+            hasKeepDebugInfo = true;
+            debuginfoExt = keepDebugInfo;
+        }
+        if ((hasKeepDebugInfo || includeDebugSymbols) && hasOmitDebugInfo) {
+            // Cannot keep and omit debug info at the same time. Note that
+            // includeDebugSymbols might already be true if configure is being run
+            // on the same plugin instance multiple times. Plugin option can
+            // repeat.
+            throw new IllegalArgumentException(
+                    PluginsResourceBundle.getMessage(resourceBundle,
+                                                     NAME + ".iae.conflict",
+                                                     NAME,
+                                                     EXCLUDE_DEBUG_INFO_ARG,
+                                                     KEEP_DEBUG_INFO_ARG));
+        }
+        if (!arg.startsWith(STRIP_CMD_ARG) &&
+            !arg.startsWith(KEEP_DEBUG_INFO_ARG) &&
+            !arg.startsWith(EXCLUDE_DEBUG_INFO_ARG)) {
+            // unknown arg value; case --strip-native-debug-symbols foobar
+            throw new IllegalArgumentException(
+                    PluginsResourceBundle.getMessage(resourceBundle,
+                                                     NAME + ".iae", NAME, arg));
+        }
+        if (!config.isEmpty()) {
+            // extraneous values; --strip-native-debug-symbols keep-debuginfo-files:foo=bar
+            throw new IllegalArgumentException(
+                    PluginsResourceBundle.getMessage(resourceBundle,
+                                                     NAME + ".iae", NAME,
+                                                     config.toString()));
+        }
+        includeDebugSymbols = hasKeepDebugInfo;
+    }
+
+    private void validateStripArg(String stripArg) throws IllegalArgumentException {
+        try {
+            Path strip = Paths.get(stripArg); // verify it's a resonable path
+            if (!Files.isExecutable(strip)) {
+                throw new IllegalArgumentException(
+                        PluginsResourceBundle.getMessage(resourceBundle,
+                                                         NAME + ".invalidstrip",
+                                                         stripArg));
+            }
+        } catch (InvalidPathException e) {
+            throw new IllegalArgumentException(
+                    PluginsResourceBundle.getMessage(resourceBundle,
+                                                     NAME + ".invalidstrip",
+                                                     e.getInput()));
+        }
+    }
+
+    private static class StrippedDebugInfoBinaryBuilder {
+
+        private final boolean includeDebug;
+        private final String debugExt;
+        private final ObjCopyCmdBuilder cmdBuilder;
+        private final String strip;
+
+        private StrippedDebugInfoBinaryBuilder(boolean includeDebug,
+                                               String debugExt,
+                                               ObjCopyCmdBuilder cmdBuilder,
+                                               String strip) {
+            this.includeDebug = includeDebug;
+            this.debugExt = debugExt;
+            this.cmdBuilder = cmdBuilder;
+            this.strip = strip;
+        }
+
+        private Optional<StrippedDebugInfoBinary> build(ResourcePoolEntry resource) {
+            Path tempDir = null;
+            Optional<ResourcePoolEntry> debugInfo = Optional.empty();
+            try {
+                Path resPath = Paths.get(resource.path());
+                String relativeFileName = resPath.getFileName().toString();
+                tempDir = Files.createTempDirectory(NAME + relativeFileName);
+                Path resourceFileBinary = tempDir.resolve(relativeFileName);
+                String relativeDbgFileName = relativeFileName + "." + debugExt;
+
+                Files.write(resourceFileBinary, resource.contentBytes());
+                Path resourceFileDebugSymbols;
+                if (includeDebug) {
+                    resourceFileDebugSymbols = tempDir.resolve(Paths.get(relativeDbgFileName));
+                    String debugEntryPath = resource.path() + "." + debugExt;
+                    byte[] debugInfoBytes = createDebugSymbolsFile(resourceFileBinary,
+                                           resourceFileDebugSymbols,
+                                           relativeDbgFileName);
+                    if (debugInfoBytes != null) {
+                        ResourcePoolEntry debugEntry = ResourcePoolEntry.create(
+                                                                debugEntryPath,
+                                                                resource.type(),
+                                                                debugInfoBytes);
+                        debugInfo = Optional.of(debugEntry);
+                    }
+                }
+                if (!stripBinary(resourceFileBinary)) {
+                    if (DEBUG) {
+                        System.err.println("DEBUG: Stripping debug info failed.");
+                    }
+                    return Optional.empty();
+                }
+                if (includeDebug && !addGnuDebugLink(tempDir,
+                                                     relativeFileName,
+                                                     relativeDbgFileName)) {
+                    if (DEBUG) {
+                        System.err.println("DEBUG: Creating debug link failed.");
+                    }
+                    return Optional.empty();
+                }
+                byte[] strippedBytes = Files.readAllBytes(resourceFileBinary);
+                ResourcePoolEntry strippedResource = resource.copyWithContent(strippedBytes);
+                return Optional.of(new StrippedDebugInfoBinary(strippedResource, debugInfo));
+            } catch (IOException | InterruptedException e) {
+                throw new PluginException(e);
+            } finally {
+                if (tempDir != null) {
+                    deleteDirRecursivelyIgnoreResult(tempDir);
+                }
+            }
+        }
+
+        /*
+         *  Equivalent of 'objcopy -g binFile'. Returning true iff stripping of the binary
+         *  succeeded.
+         */
+        private boolean stripBinary(Path binFile)
+                throws InterruptedException, IOException {
+            String filePath = binFile.toAbsolutePath().toString();
+            List<String> stripCmdLine = cmdBuilder.build(strip, STRIP_DEBUG_SYMS_OPT,
+                                                     filePath);
+            ProcessBuilder builder = createProcessBuilder(stripCmdLine);
+            Process stripProc = builder.start();
+            int retval = stripProc.waitFor();
+            return retval == 0;
+        }
+
+        /*
+         *  Equivalent of 'objcopy --add-gnu-debuglink=relativeDbgFileName binFile'.
+         *  Returning true iff adding the debug link succeeded.
+         */
+        private boolean addGnuDebugLink(Path currDir,
+                                        String binFile,
+                                        String relativeDbgFileName)
+                                                throws InterruptedException, IOException {
+            List<String> addDbgLinkCmdLine = cmdBuilder.build(strip, ADD_DEBUG_LINK_OPT +
+                                                     "=" + relativeDbgFileName,
+                                                     binFile);
+            ProcessBuilder builder = createProcessBuilder(addDbgLinkCmdLine);
+            builder.directory(currDir.toFile());
+            Process stripProc = builder.start();
+            int retval = stripProc.waitFor();
+            return retval == 0;
+
+        }
+
+        /*
+         *  Equivalent of 'objcopy --only-keep-debug binPath debugPath'.
+         *  Returning the bytes of the file containing debug symbols.
+         */
+        private byte[] createDebugSymbolsFile(Path binPath,
+                                              Path debugPath,
+                                              String dbgFileName) throws InterruptedException,
+                                                                         IOException {
+            String filePath = binPath.toAbsolutePath().toString();
+            String dbgPath = debugPath.toAbsolutePath().toString();
+            List<String> createLinkCmdLine = cmdBuilder.build(strip,
+                                                     ONLY_KEEP_DEBUG_SYMS_OPT,
+                                                     filePath,
+                                                     dbgPath);
+            ProcessBuilder builder = createProcessBuilder(createLinkCmdLine);
+            Process stripProc = builder.start();
+            int retval = stripProc.waitFor();
+            if (retval != 0) {
+                if (DEBUG) {
+                    System.err.println("DEBUG: Creating debuginfo file failed.");
+                }
+                return null;
+            } else {
+                return Files.readAllBytes(debugPath);
+            }
+        }
+
+        private ProcessBuilder createProcessBuilder(List<String> cmd) {
+            ProcessBuilder builder = new ProcessBuilder(cmd);
+            builder.redirectError(Redirect.INHERIT);
+            builder.redirectOutput(Redirect.INHERIT);
+            return builder;
+        }
+
+        private void deleteDirRecursivelyIgnoreResult(Path tempDir) {
+            try {
+                Files.walkFileTree(tempDir, new SimpleFileVisitor<Path>() {
+                    @Override
+                    public FileVisitResult visitFile(Path file,
+                            BasicFileAttributes attrs) throws IOException {
+                        Files.delete(file);
+                        return FileVisitResult.CONTINUE;
+                    }
+
+                    @Override
+                    public FileVisitResult postVisitDirectory(Path dir,
+                            IOException exc) throws IOException {
+                        Files.delete(dir);
+                        return FileVisitResult.CONTINUE;
+                    }
+                });
+            } catch (IOException e) {
+                // ignore deleting the temp dir
+            }
+        }
+
+    }
+
+    private static class StrippedDebugInfoBinary {
+        private final ResourcePoolEntry strippedBinary;
+        private final Optional<ResourcePoolEntry> debugSymbols;
+
+        private StrippedDebugInfoBinary(ResourcePoolEntry strippedBinary,
+                                        Optional<ResourcePoolEntry> debugSymbols) {
+            this.strippedBinary = Objects.requireNonNull(strippedBinary);
+            this.debugSymbols = Objects.requireNonNull(debugSymbols);
+        }
+
+        public ResourcePoolEntry strippedBinary() {
+            return strippedBinary;
+        }
+
+        public Optional<ResourcePoolEntry> debugSymbols() {
+            return debugSymbols;
+        }
+    }
+
+    // For better testing using mocked objcopy
+    public static interface ObjCopyCmdBuilder {
+        List<String> build(String objCopy, String...options);
+    }
+
+    private static final class DefaultObjCopyCmdBuilder implements ObjCopyCmdBuilder {
+
+        @Override
+        public List<String> build(String objCopy, String...options) {
+            List<String> cmdList = new ArrayList<>();
+            cmdList.add(objCopy);
+            if (options.length > 0) {
+                cmdList.addAll(Arrays.asList(options));
+            }
+            return cmdList;
+        }
+
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.jlink/linux/classes/jdk/tools/jlink/resources/strip_native_debug_symbols_plugin.properties	Thu Mar 14 14:04:39 2019 +0100
@@ -0,0 +1,46 @@
+#
+# Copyright (c) 2019, Red Hat Inc.
+# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+#
+# This code is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License version 2 only, as
+# published by the Free Software Foundation.  Oracle designates this
+# particular file as subject to the "Classpath" exception as provided
+# by Oracle in the LICENSE file that accompanied this code.
+#
+# This code is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+# version 2 for more details (a copy is included in the LICENSE file that
+# accompanied this code).
+#
+# You should have received a copy of the GNU General Public License version
+# 2 along with this work; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+# or visit www.oracle.com if you need additional information or have any
+# questions.
+#
+strip-native-debug-symbols.description=\
+Strip debug symbols from native libraries (if any). \n\
+\ This plugin requires at least one option: \n\
+\ \ \ objcopy: The path to the 'objcopy' binary. Defaults to 'objcopy' in PATH.\n\
+\ \ \ exclude-debuginfo-files: Exclude debug info files. Defaults to true.\n\
+\ \ \ keep-debuginfo-files[=<ext>]: Keep debug info files in <file>.<ext>.\n\
+\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ Defaults to <file>.debuginfo \n\
+\ Examples: --strip-native-debug-symbols keep-debuginfo-files:objcopy=/usr/bin/objcopy \n\
+\ \ \ \ \ \ \ \ \ \ \ --strip-native-debug-symbols=exclude-debuginfo-files\n
+
+strip-native-debug-symbols.argument=\
+<exclude-debuginfo-files|keep-debuginfo-files|objcopy=/path/to/objcopy>
+
+strip-native-debug-symbols.invalidstrip=Invalid objcopy command: {0}
+
+strip-native-debug-symbols.iae={0}: Unrecognized argument ''{1}''
+
+strip-native-debug-symbols.iae.conflict=\
+{0}: Cannot use ''{1}'' and ''{2}'' at the same time
+
+strip-native-debug-symbols.error.file=Error: {0}: Stripping debug info for file ''{1}'' failed.
+strip-native-debug-symbols.error.debugfile=Error: {0}: Creating debug info file for ''{1}'' failed.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.jlink/linux/classes/module-info.java.extra	Thu Mar 14 14:04:39 2019 +0100
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2019, Red Hat, Inc. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+provides jdk.tools.jlink.plugin.Plugin with
+        jdk.tools.jlink.internal.plugins.StripNativeDebugSymbolsPlugin;
--- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/TaskHelper.java	Thu Apr 25 17:27:37 2019 +0100
+++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/TaskHelper.java	Thu Mar 14 14:04:39 2019 +0100
@@ -223,6 +223,8 @@
 
     private final class PluginsHelper {
 
+        // Duplicated here so as to avoid a direct dependency on platform specific plugin
+        private static final String STRIP_NATIVE_DEBUG_SYMBOLS_NAME = "strip-native-debug-symbols";
         private ModuleLayer pluginsLayer = ModuleLayer.boot();
         private final List<Plugin> plugins;
         private String lastSorter;
@@ -424,6 +426,7 @@
             }
 
             List<Plugin> pluginsList = new ArrayList<>();
+            Set<String> seenPlugins = new HashSet<>();
             for (Entry<Plugin, List<Map<String, String>>> entry : pluginToMaps.entrySet()) {
                 Plugin plugin = entry.getKey();
                 List<Map<String, String>> argsMaps = entry.getValue();
@@ -444,7 +447,17 @@
                 }
 
                 if (!Utils.isDisabled(plugin)) {
+                    // make sure that --strip-debug and --strip-native-debug-symbols
+                    // aren't being used at the same time. --strip-debug invokes --strip-native-debug-symbols on
+                    // platforms that support it, so it makes little sense to allow both at the same time.
+                    if ((plugin instanceof DefaultStripDebugPlugin && seenPlugins.contains(STRIP_NATIVE_DEBUG_SYMBOLS_NAME)) ||
+                        (STRIP_NATIVE_DEBUG_SYMBOLS_NAME.equals(plugin.getName()) && seenPlugins.contains(DefaultStripDebugPlugin.NAME))) {
+                        throw new BadArgs("err.plugin.conflicts", "--" + DefaultStripDebugPlugin.NAME,
+                                                                "-G",
+                                                                "--" + STRIP_NATIVE_DEBUG_SYMBOLS_NAME);
+                    }
                     pluginsList.add(plugin);
+                    seenPlugins.add(plugin.getName());
                 }
             }
 
--- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/DefaultStripDebugPlugin.java	Thu Apr 25 17:27:37 2019 +0100
+++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/DefaultStripDebugPlugin.java	Thu Mar 14 14:04:39 2019 +0100
@@ -25,6 +25,11 @@
 
 package jdk.tools.jlink.internal.plugins;
 
+import java.util.Map;
+
+import jdk.tools.jlink.internal.PluginRepository;
+import jdk.tools.jlink.internal.ResourcePoolManager;
+import jdk.tools.jlink.internal.ResourcePoolManager.ResourcePoolImpl;
 import jdk.tools.jlink.plugin.Plugin;
 import jdk.tools.jlink.plugin.ResourcePool;
 import jdk.tools.jlink.plugin.ResourcePoolBuilder;
@@ -37,8 +42,22 @@
 public final class DefaultStripDebugPlugin implements Plugin {
 
     public static final String NAME = "strip-debug";
+    private static final String STRIP_NATIVE_DEBUG_PLUGIN = "strip-native-debug-symbols";
+    private static final String EXCLUDE_DEBUGINFO = "exclude-debuginfo-files";
 
-    private final Plugin javaStripPlugin = new StripJavaDebugAttributesPlugin();
+    private final Plugin javaStripPlugin;
+    private final NativePluginFactory stripNativePluginFactory;
+
+    public DefaultStripDebugPlugin() {
+        this(new StripJavaDebugAttributesPlugin(),
+             new DefaultNativePluginFactory());
+    }
+
+    public DefaultStripDebugPlugin(Plugin javaStripPlugin,
+                                   NativePluginFactory nativeStripPluginFact) {
+        this.javaStripPlugin = javaStripPlugin;
+        this.stripNativePluginFactory = nativeStripPluginFact;
+    }
 
     @Override
     public String getName() {
@@ -52,7 +71,34 @@
 
     @Override
     public ResourcePool transform(ResourcePool in, ResourcePoolBuilder out) {
-        return javaStripPlugin.transform(in, out);
+        Plugin stripNativePlugin = stripNativePluginFactory.create();
+        if (stripNativePlugin != null) {
+            Map<String, String> stripNativeConfig = Map.of(
+                                     STRIP_NATIVE_DEBUG_PLUGIN, EXCLUDE_DEBUGINFO);
+            stripNativePlugin.configure(stripNativeConfig);
+            ResourcePoolManager outRes =
+                                 new ResourcePoolManager(in.byteOrder(),
+                                                        ((ResourcePoolImpl)in).getStringTable());
+            ResourcePool strippedJava = javaStripPlugin.transform(in,
+                                                                  outRes.resourcePoolBuilder());
+            return stripNativePlugin.transform(strippedJava, out);
+        } else {
+            return javaStripPlugin.transform(in, out);
+        }
+    }
+
+    public interface NativePluginFactory {
+        Plugin create();
+    }
+
+    private static class DefaultNativePluginFactory implements NativePluginFactory {
+
+        @Override
+        public Plugin create() {
+            return PluginRepository.getPlugin(STRIP_NATIVE_DEBUG_PLUGIN,
+                                              ModuleLayer.boot());
+        }
+
     }
 
 }
--- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/PluginsResourceBundle.java	Thu Apr 25 17:27:37 2019 +0100
+++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/PluginsResourceBundle.java	Thu Mar 14 14:04:39 2019 +0100
@@ -62,7 +62,11 @@
     }
 
     public static String getMessage(String key, Object... args) throws MissingResourceException {
-        String val = pluginsBundle.getString(key);
+        return getMessage(pluginsBundle, key, args);
+    }
+
+    public static String getMessage(ResourceBundle bundle, String key, Object... args) throws MissingResourceException {
+        String val = bundle.getString(key);
         return MessageFormat.format(val, args);
     }
 }
--- a/src/jdk.jlink/share/classes/jdk/tools/jlink/resources/plugins.properties	Thu Apr 25 17:27:37 2019 +0100
+++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/resources/plugins.properties	Thu Mar 14 14:04:39 2019 +0100
@@ -209,6 +209,7 @@
 err.provider.not.functional=The provider {0} is not functional.
 
 err.plugin.mutiple.options=More than one plugin enabled by {0} option
+err.plugin.conflicts={0} ({1}) conflicts with {2}. Please use one or the other, but not both.
 err.provider.additional.arg.error=Error in additional argument specification in {0} option: {1}
 
 err.no.plugins.path=No plugins path argument.
--- a/test/jdk/tools/jlink/IntegrationTest.java	Thu Apr 25 17:27:37 2019 +0100
+++ b/test/jdk/tools/jlink/IntegrationTest.java	Thu Mar 14 14:04:39 2019 +0100
@@ -69,7 +69,7 @@
  *          jdk.jlink/jdk.tools.jimage
  *          jdk.compiler
  * @build tests.*
- * @run main IntegrationTest
+ * @run main/othervm -Xmx1g IntegrationTest
  */
 public class IntegrationTest {
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jlink/plugins/DefaultStripDebugPluginTest.java	Thu Mar 14 14:04:39 2019 +0100
@@ -0,0 +1,150 @@
+/*
+ * Copyright (c) 2019, Red Hat, Inc.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.util.Map;
+
+import jdk.tools.jlink.internal.ResourcePoolManager;
+import jdk.tools.jlink.internal.plugins.DefaultStripDebugPlugin;
+import jdk.tools.jlink.internal.plugins.DefaultStripDebugPlugin.NativePluginFactory;
+import jdk.tools.jlink.plugin.Plugin;
+import jdk.tools.jlink.plugin.ResourcePool;
+import jdk.tools.jlink.plugin.ResourcePoolBuilder;
+import jdk.tools.jlink.plugin.ResourcePoolEntry;
+import jdk.tools.jlink.plugin.ResourcePoolEntry.Type;
+
+/*
+ * @test
+ * @summary Test for combination of java debug attributes stripping and
+ * native debug symbols stripping.
+ * @modules jdk.jlink/jdk.tools.jlink.internal
+ *          jdk.jlink/jdk.tools.jlink.internal.plugins
+ *          jdk.jlink/jdk.tools.jlink.plugin
+ * @run main/othervm DefaultStripDebugPluginTest
+ */
+public class DefaultStripDebugPluginTest {
+
+    public void testWithNativeStripPresent() {
+        MockStripPlugin javaPlugin = new MockStripPlugin(false);
+        MockStripPlugin nativePlugin = new MockStripPlugin(true);
+        TestNativeStripPluginFactory nativeFactory =
+                                 new TestNativeStripPluginFactory(nativePlugin);
+        DefaultStripDebugPlugin plugin = new DefaultStripDebugPlugin(javaPlugin,
+                                                                     nativeFactory);
+        ResourcePoolManager inManager = new ResourcePoolManager();
+        ResourcePool pool = plugin.transform(inManager.resourcePool(),
+                                             inManager.resourcePoolBuilder());
+        if (!pool.findEntry(MockStripPlugin.JAVA_PATH).isPresent() ||
+            !pool.findEntry(MockStripPlugin.NATIVE_PATH).isPresent()) {
+            throw new AssertionError("Expected both native and java to get called");
+        }
+    }
+
+    public void testNoNativeStripPluginPresent() {
+        MockStripPlugin javaPlugin = new MockStripPlugin(false);
+        TestNativeStripPluginFactory nativeFactory =
+                                         new TestNativeStripPluginFactory(null);
+        DefaultStripDebugPlugin plugin = new DefaultStripDebugPlugin(javaPlugin,
+                                                                     nativeFactory);
+        ResourcePoolManager inManager = new ResourcePoolManager();
+        ResourcePool pool = plugin.transform(inManager.resourcePool(),
+                                             inManager.resourcePoolBuilder());
+        if (!pool.findEntry(MockStripPlugin.JAVA_PATH).isPresent()) {
+            throw new AssertionError("Expected java strip plugin to get called");
+        }
+    }
+
+    public static void main(String[] args) {
+        DefaultStripDebugPluginTest test = new DefaultStripDebugPluginTest();
+        test.testNoNativeStripPluginPresent();
+        test.testWithNativeStripPresent();
+    }
+
+    public static class MockStripPlugin implements Plugin {
+
+        private static final String NATIVE_PATH = "/foo/lib/test.so.debug";
+        private static final String JAVA_PATH = "/foo/TestClass.class";
+        private static final String STRIP_NATIVE_NAME = "strip-native-debug-symbols";
+        private static final String OMIT_ARG = "exclude-debuginfo-files";
+        private final boolean isNative;
+
+        MockStripPlugin(boolean isNative) {
+            this.isNative = isNative;
+        }
+
+        @Override
+        public void configure(Map<String, String> config) {
+            if (isNative) {
+                if (config.get(STRIP_NATIVE_NAME) == null ||
+                    !config.get(STRIP_NATIVE_NAME).equals(OMIT_ARG)) {
+                    throw new AssertionError("Test failed!, Expected native " +
+                                             "plugin to be properly configured.");
+                } else {
+                    System.out.println("DEBUG: native plugin properly configured with: " +
+                                       STRIP_NATIVE_NAME + "=" + config.get(STRIP_NATIVE_NAME));
+                }
+            }
+        }
+
+        @Override
+        public ResourcePool transform(ResourcePool in,
+                                      ResourcePoolBuilder out) {
+            in.transformAndCopy((r) -> {return r; }, out); // identity
+            String resPath = JAVA_PATH;
+            ResourcePoolEntry.Type type = Type.CLASS_OR_RESOURCE;
+            if (isNative) {
+                resPath = NATIVE_PATH;
+                type = Type.NATIVE_LIB;
+            }
+            ResourcePoolEntry entry = createMockEntry(resPath, type);
+            out.add(entry);
+            return out.build();
+        }
+
+        private ResourcePoolEntry createMockEntry(String path,
+                                                  ResourcePoolEntry.Type type) {
+            byte[] mockContent = new byte[] { 0, 1, 2, 3 };
+            ResourcePoolEntry entry = ResourcePoolEntry.create(path,
+                                                               type,
+                                                               mockContent);
+            return entry;
+        }
+
+    }
+
+    public static class TestNativeStripPluginFactory implements NativePluginFactory {
+
+        private final MockStripPlugin plugin;
+
+        TestNativeStripPluginFactory(MockStripPlugin plugin) {
+            this.plugin = plugin;
+        }
+
+        @Override
+        public Plugin create() {
+            return plugin;
+        }
+
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jlink/plugins/StripNativeDebugSymbolsPlugin/FakeObjCopy.java	Thu Mar 14 14:04:39 2019 +0100
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2019, Red Hat, Inc.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Fake objcopy used by StripNativeDebugSymbolsTest. It prints the
+ * passed in arguments to a log and creates a fake debug info file
+ * for --only-keep-debug invocation. Note that the first argument is
+ * the path to the log file. This argument will be omitted when
+ * logged.
+ *
+ * Callers need to ensure the log file is properly truncated.
+ *
+ */
+public class FakeObjCopy {
+
+    private static final String OBJCOPY_ONLY_KEEP_DEBUG_OPT = "--only-keep-debug";
+
+    public static void main(String[] args) throws Exception {
+        if (args.length < 1) {
+            throw new AssertionError("At least one argument expected");
+        }
+        String[] objCopyArgs = new String[args.length - 1];
+        System.arraycopy(args, 1, objCopyArgs, 0, objCopyArgs.length);
+        String logFile = args[0];
+        System.out.println("DEBUG: Fake objcopy called. Log file is: " + logFile);
+        // Log options
+        String line = Arrays.asList(objCopyArgs).stream().collect(Collectors.joining(" "));
+        Files.write(Paths.get(logFile),
+                    List.<String>of(line),
+                    StandardOpenOption.APPEND,
+                    StandardOpenOption.CREATE);
+        // Handle --only-keep-debug option as plugin attempts to read
+        // debug info file after this utility being called.
+        if (objCopyArgs.length == 3 && OBJCOPY_ONLY_KEEP_DEBUG_OPT.equals(objCopyArgs[0])) {
+            handleOnlyKeepDebug(objCopyArgs[2]);
+        }
+    }
+
+    private static void handleOnlyKeepDebug(String dbgFile) throws Exception {
+        try (PrintWriter pw = new PrintWriter(new File(dbgFile))) {
+            pw.println("Fake objcopy debug info file");
+        }
+        System.out.println("DEBUG: wrote fake debug file " + dbgFile);
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jlink/plugins/StripNativeDebugSymbolsPlugin/StripNativeDebugSymbolsPluginTest.java	Thu Mar 14 14:04:39 2019 +0100
@@ -0,0 +1,807 @@
+/*
+ * Copyright (c) 2019, Red Hat, Inc.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Scanner;
+import java.util.spi.ToolProvider;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import jdk.test.lib.compiler.CompilerUtils;
+import jdk.tools.jlink.internal.ResourcePoolManager;
+import jdk.tools.jlink.internal.plugins.StripNativeDebugSymbolsPlugin;
+import jdk.tools.jlink.internal.plugins.StripNativeDebugSymbolsPlugin.ObjCopyCmdBuilder;
+import jdk.tools.jlink.plugin.ResourcePool;
+import jdk.tools.jlink.plugin.ResourcePoolEntry;
+
+/*
+ * @test
+ * @requires os.family == "linux"
+ * @bug 8214796
+ * @summary Test --strip-native-debug-symbols plugin
+ * @library /test/lib
+ * @modules jdk.compiler
+ *          jdk.jlink/jdk.tools.jlink.internal.plugins
+ *          jdk.jlink/jdk.tools.jlink.internal
+ *          jdk.jlink/jdk.tools.jlink.plugin
+ * @build jdk.test.lib.compiler.CompilerUtils FakeObjCopy
+ * @run main/othervm -Xmx1g StripNativeDebugSymbolsPluginTest
+ */
+public class StripNativeDebugSymbolsPluginTest {
+
+    private static final String OBJCOPY = "objcopy";
+    private static final String DEFAULT_OBJCOPY_CMD = OBJCOPY;
+    private static final String PLUGIN_NAME = "strip-native-debug-symbols";
+    private static final String MODULE_NAME_WITH_NATIVE = "fib";
+    private static final String JAVA_HOME = System.getProperty("java.home");
+    private static final String NATIVE_LIB_NAME = "libFib.so";
+    private static final Path JAVA_LIB_PATH = Paths.get(System.getProperty("java.library.path"));
+    private static final Path LIB_FIB_SRC = JAVA_LIB_PATH.resolve(NATIVE_LIB_NAME);
+    private static final String FIBJNI_CLASS_NAME = "FibJNI.java";
+    private static final Path JAVA_SRC_DIR = Paths.get(System.getProperty("test.src"))
+                                                  .resolve("src")
+                                                  .resolve(MODULE_NAME_WITH_NATIVE);
+    private static final Path FIBJNI_JAVA_CLASS = JAVA_SRC_DIR.resolve(FIBJNI_CLASS_NAME);
+    private static final String DEBUG_EXTENSION = "debug";
+    private static final long ORIG_LIB_FIB_SIZE = LIB_FIB_SRC.toFile().length();
+    private static final String FAKE_OBJ_COPY_LOG_FILE = "objcopy.log";
+    private static final String OBJCOPY_ONLY_DEBUG_SYMS_OPT = "-g";
+    private static final String OBJCOPY_ONLY_KEEP_DEBUG_SYMS_OPT = "--only-keep-debug";
+    private static final String OBJCOPY_ADD_DEBUG_LINK_OPT = "--add-gnu-debuglink";
+
+    ///////////////////////////////////////////////////////////////////////////
+    //
+    // Tests which do NOT rely on objcopy being present on the test system
+    //
+    ///////////////////////////////////////////////////////////////////////////
+
+    public void testPluginLoaded() {
+        List<String> output =
+            JLink.run("--list-plugins").output();
+        if (output.stream().anyMatch(s -> s.contains(PLUGIN_NAME))) {
+            System.out.println("DEBUG: " + PLUGIN_NAME + " plugin loaded as expected.");
+        } else {
+            throw new AssertionError("strip-native-debug-symbols plugin not in " +
+                                     "--list-plugins output.");
+        }
+    }
+
+    public void testConfigureFakeObjCopy() throws Exception {
+        configureConflictingOptions();
+        configureObjcopyWithOmit();
+        configureObjcopyWithKeep();
+        configureUnknownOptions();
+        configureMultipleTimesSamePlugin();
+        System.out.println("Test testConfigureFakeObjCopy() PASSED!");
+    }
+
+    private void configureMultipleTimesSamePlugin() throws Exception {
+        Map<String, String> keepDebug = Map.of(
+                StripNativeDebugSymbolsPlugin.NAME, "keep-debuginfo-files"
+        );
+        Map<String, String> excludeDebug = Map.of(
+                StripNativeDebugSymbolsPlugin.NAME, "exclude-debuginfo-files"
+        );
+        StripNativeDebugSymbolsPlugin plugin = createAndConfigPlugin(keepDebug);
+        try {
+            plugin.doConfigure(false, excludeDebug);
+            throw new AssertionError("should have thrown IAE for broken config: " +
+                                     keepDebug + " and " + excludeDebug);
+        } catch (IllegalArgumentException e) {
+            // pass
+            System.out.println("DEBUG: test threw IAE " + e.getMessage() +
+                               " as expected.");
+        }
+    }
+
+    private void configureUnknownOptions() throws Exception {
+        Map<String, String> config = Map.of(
+                StripNativeDebugSymbolsPlugin.NAME, "foobar"
+        );
+        doConfigureUnknownOption(config);
+        config = Map.of(
+                StripNativeDebugSymbolsPlugin.NAME, "keep-debuginfo-files",
+                "foo", "bar" // unknown value
+        );
+        doConfigureUnknownOption(config);
+    }
+
+    private void doConfigureUnknownOption(Map<String, String> config) throws Exception {
+        try {
+            createAndConfigPlugin(config);
+            throw new AssertionError("should have thrown IAE for broken config: " + config);
+        } catch (IllegalArgumentException e) {
+            // pass
+            System.out.println("DEBUG: test threw IAE " + e.getMessage() +
+                               " as expected.");
+        }
+    }
+
+    private void configureObjcopyWithKeep() throws Exception {
+        String objcopyPath = "foobar";
+        String debugExt = "debuginfo"; // that's the default value
+        Map<String, String> config = Map.of(
+                StripNativeDebugSymbolsPlugin.NAME, "keep-debuginfo-files",
+                "objcopy", objcopyPath
+        );
+        doKeepDebugInfoFakeObjCopyTest(config, debugExt, objcopyPath);
+        // Do it again combining options the other way round
+        debugExt = "testme";
+        config = Map.of(
+                StripNativeDebugSymbolsPlugin.NAME, "objcopy=" + objcopyPath,
+                "keep-debuginfo-files", debugExt
+        );
+        doKeepDebugInfoFakeObjCopyTest(config, debugExt, objcopyPath);
+        System.out.println("DEBUG: configureObjcopyWithKeep() PASSED!");
+    }
+
+    private void configureObjcopyWithOmit() throws Exception {
+        String objcopyPath = "something-non-standard";
+        Map<String, String> config = Map.of(
+                StripNativeDebugSymbolsPlugin.NAME, "exclude-debuginfo-files",
+                "objcopy", objcopyPath
+        );
+        doOmitDebugInfoFakeObjCopyTest(config, objcopyPath);
+        System.out.println("DEBUG: configureObjcopyWithOmit() PASSED!");
+    }
+
+    private void configureConflictingOptions() throws Exception {
+        Map<String, String> config = Map.of(
+                StripNativeDebugSymbolsPlugin.NAME, "exclude-debuginfo-files",
+                "keep-debuginfo-files", "foo-ext"
+        );
+        doConfigureConflictingOptions(config);
+        config = Map.of(
+                StripNativeDebugSymbolsPlugin.NAME, "exclude-debuginfo-files=bar",
+                "keep-debuginfo-files", "foo-ext"
+        );
+        doConfigureConflictingOptions(config);
+    }
+
+    private void doConfigureConflictingOptions(Map<String, String> config) throws Exception {
+        try {
+            createAndConfigPlugin(config);
+            throw new AssertionError("keep-debuginfo-files and exclude-debuginfo-files " +
+                                     " should have conflicted!");
+        } catch (IllegalArgumentException e) {
+            // pass
+            if (e.getMessage().contains("keep-debuginfo-files") &&
+                    e.getMessage().contains("exclude-debuginfo-files")) {
+                System.out.println("DEBUG: test threw IAE " + e.getMessage() +
+                               " as expected.");
+            } else {
+                throw new AssertionError("Unexpected IAE", e);
+            }
+        }
+    }
+
+    public void testTransformFakeObjCopyNoDebugInfoFiles() throws Exception {
+        Map<String, String> defaultConfig = Map.of(
+                                 StripNativeDebugSymbolsPlugin.NAME, "exclude-debuginfo-files"
+                                 );
+        doOmitDebugInfoFakeObjCopyTest(defaultConfig, DEFAULT_OBJCOPY_CMD);
+        System.out.println("testTransformFakeObjCopyNoDebugInfoFiles() PASSED!");
+    }
+
+    private void doOmitDebugInfoFakeObjCopyTest(Map<String, String> config,
+                                                String expectedObjCopy) throws Exception {
+        StripNativeDebugSymbolsPlugin plugin = createAndConfigPlugin(config, expectedObjCopy);
+        String binFile = "mybin";
+        String path = "/fib/bin/" + binFile;
+        ResourcePoolEntry debugEntry = createMockEntry(path,
+                                                       ResourcePoolEntry.Type.NATIVE_CMD);
+        ResourcePoolManager inResources = new ResourcePoolManager();
+        ResourcePoolManager outResources = new ResourcePoolManager();
+        inResources.add(debugEntry);
+        ResourcePool output = plugin.transform(
+                                        inResources.resourcePool(),
+                                        outResources.resourcePoolBuilder());
+        // expect entry to be present
+        if (output.findEntry(path).isPresent()) {
+            System.out.println("DEBUG: File " + path + " present as exptected.");
+        } else {
+            throw new AssertionError("Test failed. Binary " + path +
+                                     " not present after stripping!");
+        }
+        verifyFakeObjCopyCalled(binFile);
+    }
+
+    public void testTransformFakeObjCopyKeepDebugInfoFiles() throws Exception {
+        Map<String, String> defaultConfig = Map.of(
+                                 StripNativeDebugSymbolsPlugin.NAME,
+                                 "keep-debuginfo-files=" + DEBUG_EXTENSION
+                                 );
+        doKeepDebugInfoFakeObjCopyTest(defaultConfig,
+                                       DEBUG_EXTENSION,
+                                       DEFAULT_OBJCOPY_CMD);
+        System.out.println("testTransformFakeObjCopyKeepDebugInfoFiles() PASSED!");
+    }
+
+    private void doKeepDebugInfoFakeObjCopyTest(Map<String, String> config,
+                                                String debugExt,
+                                                String expectedObjCopy) throws Exception {
+        StripNativeDebugSymbolsPlugin plugin = createAndConfigPlugin(config, expectedObjCopy);
+        String sharedLib = "myLib.so";
+        String path = "/fib/lib/" + sharedLib;
+        ResourcePoolEntry debugEntry = createMockEntry(path,
+                                                       ResourcePoolEntry.Type.NATIVE_LIB);
+        ResourcePoolManager inResources = new ResourcePoolManager();
+        ResourcePoolManager outResources = new ResourcePoolManager();
+        inResources.add(debugEntry);
+        ResourcePool output = plugin.transform(
+                                        inResources.resourcePool(),
+                                        outResources.resourcePoolBuilder());
+        // expect entry + debug info entry to be present
+        String debugPath = path + "." + debugExt;
+        if (output.findEntry(path).isPresent() &&
+            output.findEntry(debugPath).isPresent()) {
+            System.out.println("DEBUG: Files " + path + "{,." + debugExt +
+                               "} present as exptected.");
+        } else {
+            throw new AssertionError("Test failed. Binary files " + path +
+                                     "{,." + debugExt +"} not present after " +
+                                     "stripping!");
+        }
+        verifyFakeObjCopyCalledMultiple(sharedLib, debugExt);
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    //
+    // Tests which DO rely on objcopy being present on the test system.
+    // Skipped otherwise.
+    //
+    ///////////////////////////////////////////////////////////////////////////
+
+    public void testStripNativeLibraryDefaults() throws Exception {
+        if (!hasJmods()) return;
+
+        Path libFibJmod = createLibFibJmod();
+
+        Path imageDir = Paths.get("stripped-native-libs");
+        JLink.run("--output", imageDir.toString(),
+                "--verbose",
+                "--module-path", modulePathWith(libFibJmod),
+                "--add-modules", MODULE_NAME_WITH_NATIVE,
+                "--strip-native-debug-symbols=exclude-debuginfo-files").output();
+        Path libDir = imageDir.resolve("lib");
+        Path postStripLib = libDir.resolve(NATIVE_LIB_NAME);
+        long postStripSize = postStripLib.toFile().length();
+
+        if (postStripSize == 0) {
+            throw new AssertionError("Lib file size 0. Test error?!");
+        }
+        // Heuristic: libLib.so is smaller post debug info stripping
+        if (postStripSize >= ORIG_LIB_FIB_SIZE) {
+            throw new AssertionError("Expected native library stripping to " +
+                                     "reduce file size. Expected < " +
+                                     ORIG_LIB_FIB_SIZE + ", got: " + postStripSize);
+        } else {
+            System.out.println("DEBUG: File size of " + postStripLib.toString() +
+                    " " + postStripSize + " < " + ORIG_LIB_FIB_SIZE + " as expected." );
+        }
+        verifyFibModule(imageDir); // Sanity check fib module which got libFib.so stripped
+        System.out.println("DEBUG: testStripNativeLibraryDefaults() PASSED!");
+    }
+
+    public void testOptionsInvalidObjcopy() throws Exception {
+        if (!hasJmods()) return;
+
+        Path libFibJmod = createLibFibJmod();
+
+        String notExists = "/do/not/exist/objcopy";
+
+        Path imageDir = Paths.get("invalid-objcopy-command");
+        String[] jlinkCmdArray = new String[] {
+                JAVA_HOME + File.separator + "bin" + File.separator + "jlink",
+                "--output", imageDir.toString(),
+                "--verbose",
+                "--module-path", modulePathWith(libFibJmod),
+                "--add-modules", MODULE_NAME_WITH_NATIVE,
+                "--strip-native-debug-symbols", "objcopy=" + notExists,
+        };
+        List<String> jlinkCmd = Arrays.asList(jlinkCmdArray);
+        System.out.println("Debug: command: " + jlinkCmd.stream().collect(
+                                                    Collectors.joining(" ")));
+        ProcessBuilder builder = new ProcessBuilder(jlinkCmd);
+        Process p = builder.start();
+        int status = p.waitFor();
+        if (status == 0) {
+            throw new AssertionError("Expected jlink to fail!");
+        } else {
+            verifyInvalidObjcopyError(p.getInputStream(), notExists);
+            System.out.println("DEBUG: testOptionsInvalidObjcopy() PASSED!");
+        }
+    }
+
+    public void testStripNativeLibsDebugSymsIncluded() throws Exception {
+        if (!hasJmods()) return;
+
+        Path libFibJmod = createLibFibJmod();
+
+        Path imageDir = Paths.get("stripped-native-libs-with-debug");
+        JLink.run("--output", imageDir.toString(),
+                "--verbose",
+                "--module-path", modulePathWith(libFibJmod),
+                "--add-modules", MODULE_NAME_WITH_NATIVE,
+                "--strip-native-debug-symbols",
+                "keep-debuginfo-files=" + DEBUG_EXTENSION);
+
+        Path libDir = imageDir.resolve("lib");
+        Path postStripLib = libDir.resolve(NATIVE_LIB_NAME);
+        long postStripSize = postStripLib.toFile().length();
+
+        if (postStripSize == 0) {
+            throw new AssertionError("Lib file size 0. Test error?!");
+        }
+        // Heuristic: libLib.so is smaller post debug info stripping
+        if (postStripSize >= ORIG_LIB_FIB_SIZE) {
+            throw new AssertionError("Expected native library stripping to " +
+                                     "reduce file size. Expected < " +
+                                     ORIG_LIB_FIB_SIZE + ", got: " + postStripSize);
+        } else {
+            System.out.println("DEBUG: File size of " + postStripLib.toString() +
+                    " " + postStripSize + " < " + ORIG_LIB_FIB_SIZE + " as expected." );
+        }
+        // stripped with option to preserve debug symbols file
+        verifyDebugInfoSymbolFilePresent(imageDir);
+        System.out.println("DEBUG: testStripNativeLibsDebugSymsIncluded() PASSED!");
+    }
+
+    private void verifyFakeObjCopyCalledMultiple(String expectedFile,
+                                                 String dbgExt) throws Exception {
+        // transform of the StripNativeDebugSymbolsPlugin created objcopy.log
+        // with our stubbed FakeObjCopy. See FakeObjCopy.java
+        List<String> allLines = Files.readAllLines(Paths.get(FAKE_OBJ_COPY_LOG_FILE));
+        if (allLines.size() != 3) {
+            throw new AssertionError("Expected 3 calls to objcopy");
+        }
+        // 3 calls to objcopy are as follows:
+        //    1. Only keep debug symbols
+        //    2. Strip debug symbols
+        //    3. Add debug link to stripped file
+        String onlyKeepDebug = allLines.get(0);
+        String stripSymbolsLine = allLines.get(1);
+        String addGnuDebugLink = allLines.get(2);
+        System.out.println("DEBUG: Inspecting fake objcopy calls: " + allLines);
+        boolean passed = stripSymbolsLine.startsWith(OBJCOPY_ONLY_DEBUG_SYMS_OPT);
+        passed &= stripSymbolsLine.endsWith(expectedFile);
+        String[] tokens = onlyKeepDebug.split("\\s");
+        passed &= tokens[0].equals(OBJCOPY_ONLY_KEEP_DEBUG_SYMS_OPT);
+        passed &= tokens[1].endsWith(expectedFile);
+        passed &= tokens[2].endsWith(expectedFile + "." + dbgExt);
+        tokens = addGnuDebugLink.split("\\s");
+        String[] addDbgTokens = tokens[0].split("=");
+        passed &= addDbgTokens[1].equals(expectedFile + "." + dbgExt);
+        passed &= addDbgTokens[0].equals(OBJCOPY_ADD_DEBUG_LINK_OPT);
+        passed &= tokens[1].endsWith(expectedFile);
+        if (!passed) {
+            throw new AssertionError("Test failed! objcopy not properly called " +
+                                     "with expected options!");
+        }
+    }
+
+    private void verifyFakeObjCopyCalled(String expectedFile) throws Exception {
+        // transform of the StripNativeDebugSymbolsPlugin created objcopy.log
+        // with our stubbed FakeObjCopy. See FakeObjCopy.java
+        List<String> allLines = Files.readAllLines(Paths.get(FAKE_OBJ_COPY_LOG_FILE));
+        if (allLines.size() != 1) {
+            throw new AssertionError("Expected 1 call to objcopy only");
+        }
+        String optionLine = allLines.get(0);
+        System.out.println("DEBUG: Inspecting fake objcopy arguments: " + optionLine);
+        boolean passed = optionLine.startsWith(OBJCOPY_ONLY_DEBUG_SYMS_OPT);
+        passed &= optionLine.endsWith(expectedFile);
+        if (!passed) {
+            throw new AssertionError("Test failed! objcopy not called with " +
+                                     "expected options!");
+        }
+    }
+
+    private ResourcePoolEntry createMockEntry(String path,
+                                              ResourcePoolEntry.Type type) {
+        byte[] mockContent = new byte[] { 0, 1, 2, 3 };
+        ResourcePoolEntry entry = ResourcePoolEntry.create(
+                path,
+                type,
+                mockContent);
+        return entry;
+    }
+
+    private StripNativeDebugSymbolsPlugin createAndConfigPlugin(
+                                            Map<String, String> config,
+                                            String expectedObjcopy)
+                                            throws IOException {
+        TestObjCopyCmdBuilder cmdBuilder = new TestObjCopyCmdBuilder(expectedObjcopy);
+        return createAndConfigPlugin(config, cmdBuilder);
+    }
+
+    private StripNativeDebugSymbolsPlugin createAndConfigPlugin(
+            Map<String, String> config) throws IOException {
+        TestObjCopyCmdBuilder cmdBuilder = new TestObjCopyCmdBuilder();
+        return createAndConfigPlugin(config, cmdBuilder);
+    }
+
+    private StripNativeDebugSymbolsPlugin createAndConfigPlugin(
+                                Map<String, String> config,
+                                TestObjCopyCmdBuilder builder) throws IOException {
+        StripNativeDebugSymbolsPlugin plugin =
+                                     new StripNativeDebugSymbolsPlugin(builder);
+        plugin.doConfigure(false, config);
+        return plugin;
+    }
+
+    // Create the jmod with the native library
+    private Path createLibFibJmod() throws IOException {
+        JmodFileBuilder jmodBuilder = new JmodFileBuilder(MODULE_NAME_WITH_NATIVE);
+        jmodBuilder.javaClass(FIBJNI_JAVA_CLASS);
+        jmodBuilder.nativeLib(LIB_FIB_SRC);
+        return jmodBuilder.build();
+    }
+
+    private String modulePathWith(Path jmod) {
+        return Paths.get(JAVA_HOME, "jmods").toString() +
+                    File.pathSeparator + jmod.getParent().toString();
+    }
+
+    private boolean hasJmods() {
+        if (!Files.exists(Paths.get(JAVA_HOME, "jmods"))) {
+            System.err.println("Test skipped. NO jmods directory");
+            return false;
+        }
+        return true;
+    }
+
+    private void verifyInvalidObjcopyError(InputStream errInput, String match) {
+        boolean foundMatch = false;
+        try (Scanner scanner = new Scanner(errInput)) {
+            while (scanner.hasNextLine()) {
+                String line = scanner.nextLine();
+                System.out.println("DEBUG: >>>> " + line);
+                if (line.contains(match)) {
+                    foundMatch = true;
+                    break;
+                }
+            }
+        }
+        if (!foundMatch) {
+            throw new AssertionError("Expected to find " + match +
+                                    " in error stream.");
+        } else {
+            System.out.println("DEBUG: Found string " + match + " as expected.");
+        }
+    }
+
+    private void verifyDebugInfoSymbolFilePresent(Path image)
+                                    throws IOException, InterruptedException {
+        Path debugSymsFile = image.resolve("lib/libFib.so.debug");
+        if (!Files.exists(debugSymsFile)) {
+            throw new AssertionError("Expected stripped debug info file " +
+                                        debugSymsFile.toString() + " to exist.");
+        }
+        long debugSymsSize = debugSymsFile.toFile().length();
+        if (debugSymsSize <= 0) {
+            throw new AssertionError("sanity check for fib.FibJNI failed " +
+                                     "post-stripping!");
+        } else {
+            System.out.println("DEBUG: Debug symbols stripped from libFib.so " +
+                               "present (" + debugSymsFile.toString() + ") as expected.");
+        }
+    }
+
+    private void verifyFibModule(Path image)
+                                throws IOException, InterruptedException {
+        System.out.println("DEBUG: sanity checking fib module...");
+        Path launcher = image.resolve("bin/java");
+        List<String> args = new ArrayList<>();
+        args.add(launcher.toString());
+        args.add("--add-modules");
+        args.add(MODULE_NAME_WITH_NATIVE);
+        args.add("fib.FibJNI");
+        args.add("7");
+        args.add("13"); // fib(7) == 13
+        System.out.println("DEBUG: [command] " +
+                                args.stream().collect(Collectors.joining(" ")));
+        Process proc = new ProcessBuilder(args).inheritIO().start();
+        int status = proc.waitFor();
+        if (status == 0) {
+            System.out.println("DEBUG: sanity checking fib module... PASSED!");
+        } else {
+            throw new AssertionError("sanity check for fib.FibJNI failed post-" +
+                                     "stripping!");
+        }
+    }
+
+    private static boolean isObjcopyPresent() throws Exception {
+        String[] objcopyVersion = new String[] {
+                OBJCOPY, "--version",
+        };
+        List<String> command = Arrays.asList(objcopyVersion);
+        try {
+            ProcessBuilder builder = new ProcessBuilder(command);
+            builder.inheritIO();
+            Process p = builder.start();
+            int status = p.waitFor();
+            if (status != 0) {
+                System.out.println("Debug: objcopy binary doesn't seem to be " +
+                                   "present or functional.");
+                return false;
+            }
+        } catch (IOException e) {
+            System.out.println("Debug: objcopy binary doesn't seem to be present " +
+                               "or functional.");
+            return false;
+        }
+        return true;
+    }
+
+    public static void main(String[] args) throws Exception {
+        StripNativeDebugSymbolsPluginTest test = new StripNativeDebugSymbolsPluginTest();
+        if (isObjcopyPresent()) {
+            test.testStripNativeLibraryDefaults();
+            test.testStripNativeLibsDebugSymsIncluded();
+            test.testOptionsInvalidObjcopy();
+        } else {
+            System.out.println("DEBUG: objcopy binary not available. " +
+                               "Running reduced set of tests.");
+        }
+        test.testTransformFakeObjCopyNoDebugInfoFiles();
+        test.testTransformFakeObjCopyKeepDebugInfoFiles();
+        test.testConfigureFakeObjCopy();
+        test.testPluginLoaded();
+    }
+
+    static class JLink {
+        static final ToolProvider JLINK_TOOL = ToolProvider.findFirst("jlink")
+            .orElseThrow(() ->
+                new RuntimeException("jlink tool not found")
+            );
+
+        static JLink run(String... options) {
+            JLink jlink = new JLink();
+            if (jlink.execute(options) != 0) {
+                throw new AssertionError("Jlink expected to exit with 0 return code");
+            }
+            return jlink;
+        }
+
+        final List<String> output = new ArrayList<>();
+        private int execute(String... options) {
+            System.out.println("jlink " +
+                Stream.of(options).collect(Collectors.joining(" ")));
+
+            StringWriter writer = new StringWriter();
+            PrintWriter pw = new PrintWriter(writer);
+            int rc = JLINK_TOOL.run(pw, pw, options);
+            System.out.println(writer.toString());
+            Stream.of(writer.toString().split("\\v"))
+                  .map(String::trim)
+                  .forEach(output::add);
+            return rc;
+        }
+
+        boolean contains(String s) {
+            return output.contains(s);
+        }
+
+        List<String> output() {
+            return output;
+        }
+    }
+
+    /**
+     * Builder to create JMOD file
+     */
+    private static class JmodFileBuilder {
+
+        private static final ToolProvider JMOD_TOOL = ToolProvider
+                .findFirst("jmod")
+                .orElseThrow(() ->
+                    new RuntimeException("jmod tool not found")
+                );
+        private static final Path SRC_DIR = Paths.get("src");
+        private static final Path MODS_DIR = Paths.get("mod");
+        private static final Path JMODS_DIR = Paths.get("jmods");
+        private static final Path LIBS_DIR = Paths.get("libs");
+
+        private final String name;
+        private final List<Path> nativeLibs = new ArrayList<>();
+        private final List<Path> javaClasses = new ArrayList<>();
+
+        private JmodFileBuilder(String name) throws IOException {
+            this.name = name;
+
+            deleteDirectory(MODS_DIR);
+            deleteDirectory(SRC_DIR);
+            deleteDirectory(LIBS_DIR);
+            deleteDirectory(JMODS_DIR);
+            Path msrc = SRC_DIR.resolve(name);
+            if (Files.exists(msrc)) {
+                deleteDirectory(msrc);
+            }
+        }
+
+        JmodFileBuilder nativeLib(Path libFileSrc) {
+            nativeLibs.add(libFileSrc);
+            return this;
+        }
+
+        JmodFileBuilder javaClass(Path srcPath) {
+            javaClasses.add(srcPath);
+            return this;
+        }
+
+        Path build() throws IOException {
+            compileModule();
+            return createJmodFile();
+        }
+
+        private void compileModule() throws IOException  {
+            Path msrc = SRC_DIR.resolve(name);
+            Files.createDirectories(msrc);
+            // copy class using native lib to expected path
+            if (javaClasses.size() > 0) {
+                for (Path srcPath: javaClasses) {
+                    Path targetPath = msrc.resolve(srcPath.getFileName());
+                    Files.copy(srcPath, targetPath);
+                }
+            }
+            // generate module-info file.
+            Path minfo = msrc.resolve("module-info.java");
+            try (BufferedWriter bw = Files.newBufferedWriter(minfo);
+                 PrintWriter writer = new PrintWriter(bw)) {
+                writer.format("module %s { }%n", name);
+            }
+
+            if (!CompilerUtils.compile(msrc, MODS_DIR,
+                                             "--module-source-path",
+                                             SRC_DIR.toString())) {
+
+            }
+        }
+
+        private Path createJmodFile() throws IOException {
+            Path mclasses = MODS_DIR.resolve(name);
+            Files.createDirectories(JMODS_DIR);
+            Path outfile = JMODS_DIR.resolve(name + ".jmod");
+            List<String> args = new ArrayList<>();
+            args.add("create");
+            // add classes
+            args.add("--class-path");
+            args.add(mclasses.toString());
+            // native libs
+            if (nativeLibs.size() > 0) {
+                // Copy the JNI library to the expected path
+                Files.createDirectories(LIBS_DIR);
+                for (Path srcLib: nativeLibs) {
+                    Path targetLib = LIBS_DIR.resolve(srcLib.getFileName());
+                    Files.copy(srcLib, targetLib);
+                }
+                args.add("--libs");
+                args.add(LIBS_DIR.toString());
+            }
+            args.add(outfile.toString());
+
+            if (Files.exists(outfile)) {
+                Files.delete(outfile);
+            }
+
+            System.out.println("jmod " +
+                args.stream().collect(Collectors.joining(" ")));
+
+            int rc = JMOD_TOOL.run(System.out, System.out,
+                                   args.toArray(new String[args.size()]));
+            if (rc != 0) {
+                throw new AssertionError("jmod failed: rc = " + rc);
+            }
+            return outfile;
+        }
+
+        private static void deleteDirectory(Path dir) throws IOException {
+            try {
+                Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
+                    @Override
+                    public FileVisitResult visitFile(Path file,
+                                                     BasicFileAttributes attrs)
+                        throws IOException
+                    {
+                        Files.delete(file);
+                        return FileVisitResult.CONTINUE;
+                    }
+
+                    @Override
+                    public FileVisitResult postVisitDirectory(Path dir,
+                                                              IOException exc)
+                        throws IOException
+                    {
+                        Files.delete(dir);
+                        return FileVisitResult.CONTINUE;
+                    }
+                });
+            } catch (NoSuchFileException e) {
+                // ignore non-existing files
+            }
+        }
+    }
+
+    private static class TestObjCopyCmdBuilder implements ObjCopyCmdBuilder {
+
+        private final String expectedObjCopy;
+        private final String logFile;
+
+        TestObjCopyCmdBuilder() {
+            this(DEFAULT_OBJCOPY_CMD);
+        }
+        TestObjCopyCmdBuilder(String exptectedObjCopy) {
+            Path logFilePath = Paths.get(FAKE_OBJ_COPY_LOG_FILE);
+            try {
+                Files.deleteIfExists(logFilePath);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+            this.logFile = logFilePath.toFile().getAbsolutePath();
+            this.expectedObjCopy = exptectedObjCopy;
+        }
+
+        @Override
+        public List<String> build(String objCopy, String... options) {
+            if (!expectedObjCopy.equals(objCopy)) {
+                throw new AssertionError("Expected objcopy to be '" +
+                                         expectedObjCopy + "' but was '" +
+                                         objCopy);
+            }
+            List<String> fakeObjCopy = new ArrayList<>();
+            fakeObjCopy.add(JAVA_HOME + File.separator + "bin" + File.separator + "java");
+            fakeObjCopy.add("-cp");
+            fakeObjCopy.add(System.getProperty("test.classes"));
+            fakeObjCopy.add("FakeObjCopy");
+            // Note that adding the gnu debug link changes the PWD of the
+            // java process calling FakeObjCopy. As such we need to pass in the
+            // log file path this way. Relative paths won't work as it would be
+            // relative to the temporary directory which gets deleted post
+            // adding the debug link
+            fakeObjCopy.add(logFile);
+            if (options.length > 0) {
+                fakeObjCopy.addAll(Arrays.asList(options));
+            }
+            return fakeObjCopy;
+        }
+
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jlink/plugins/StripNativeDebugSymbolsPlugin/src/fib/FibJNI.java	Thu Mar 14 14:04:39 2019 +0100
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2019, Red Hat, Inc
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package fib;
+public class FibJNI {
+
+    static {
+        // Native lib used for debug symbols stripping
+        System.loadLibrary("Fib");
+    }
+
+    private final int num;
+    private final long expected;
+
+    public FibJNI(int num, long expected) {
+        this.num = num;
+        this.expected = expected;
+    }
+
+    public void callNative() {
+        callJNI(this, num);
+    }
+
+    // Called from JNI library libFib
+    private void callback(long val) {
+        System.out.println("Debug: result was: " + val);
+        if (val != expected) {
+            throw new RuntimeException("Expected " + expected + " but got: " +val);
+        }
+    }
+
+    public static native void callJNI(Object target, int num);
+
+    public static void main(String[] args) {
+        if (args.length != 2) {
+            System.err.println("Usage: " + FibJNI.class.getName() + " <input> <expectedResult>");
+        }
+        int input = Integer.parseInt(args[0]);
+        long expected = Long.parseLong(args[1]);
+        FibJNI fib = new FibJNI(input, expected);
+        fib.callNative();
+        System.out.println("DEBUG: Sanity check for " + FibJNI.class.getSimpleName() + " passed.");
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jlink/plugins/StripNativeDebugSymbolsPlugin/src/libFib.c	Thu Mar 14 14:04:39 2019 +0100
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2019, Red Hat, Inc
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+#include <jni.h>
+
+static jlong fib(jint num) {
+    if (num == 0) {
+        return 0;
+    }
+    if (num <= 2) {
+        return 1;
+    }
+    return fib(num - 2) + fib(num -1);
+}
+
+static void callCallback(JNIEnv *env, jclass cls, jobject target, jlong result) {
+    jmethodID mid = (*env)->GetMethodID(env, cls, "callback", "(J)V");
+    if (mid == NULL) {
+        jclass nsme = (jclass) (*env)->NewGlobalRef(env,
+                                                    (*env)->FindClass(env,
+                                                                      "java/lang/NoSuchMethodException"));
+        if (nsme != NULL) {
+            (*env)->ThrowNew(env, nsme, "Can't find method callback()");
+        }
+        return;
+    }
+    (*env)->CallVoidMethod(env, target, mid, result);
+}
+
+static void calculateAndCallCallback(JNIEnv *env, jclass cls, jobject target, jint num) {
+    jlong result = -1;
+    result = fib(num);
+    callCallback(env, cls, target, result);
+}
+
+JNIEXPORT void JNICALL
+Java_fib_FibJNI_callJNI(JNIEnv *env, jclass cls, jobject target, jint num) {
+    calculateAndCallCallback(env, cls, target, num);
+}