8231280: Linux packages produced by jpackage should have correct dependencies JDK-8200758-branch
authorherrick
Mon, 30 Sep 2019 19:33:13 -0400
branchJDK-8200758-branch
changeset 58417 67ffaf3a2b75
parent 58416 f09bf58c1f17
child 58418 7cd20dbeee36
8231280: Linux packages produced by jpackage should have correct dependencies Submitted-by: asemenyuk Reviewed-by: herrick
src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LibProvidersLookup.java
src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppBundler.java
src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppImageBuilder.java
src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java
src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageBundler.java
src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmBundler.java
src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/LinuxResources.properties
src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/LinuxResources_ja.properties
src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/LinuxResources_zh_CN.properties
src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.control
src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.spec
src/jdk.jpackage/share/classes/jdk/jpackage/internal/ConfigException.java
src/jdk.jpackage/share/classes/jdk/jpackage/internal/Executor.java
src/jdk.jpackage/share/classes/jdk/jpackage/internal/I18N.java
src/jdk.jpackage/share/classes/jdk/jpackage/internal/IOUtils.java
src/jdk.jpackage/share/classes/jdk/jpackage/internal/ToolValidator.java
src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties
src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources_ja.properties
src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources_zh_CN.properties
test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java
test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java
test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java
test/jdk/tools/jpackage/junit/jdk/jpackage/internal/ToolValidatorTest.java
test/jdk/tools/jpackage/junit/junit.java
test/jdk/tools/jpackage/linux/PackageDepsTest.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LibProvidersLookup.java	Mon Sep 30 19:33:13 2019 -0400
@@ -0,0 +1,163 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. 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.
+ */
+package jdk.jpackage.internal;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.function.Predicate;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Builds list of packages providing dynamic libraries for the given set of files.
+ */
+final public class LibProvidersLookup {
+    static boolean supported() {
+        return (new ToolValidator(TOOL_LDD).validate() == null);
+    }
+
+    public LibProvidersLookup() {
+    }
+
+    LibProvidersLookup setPackageLookup(PackageLookup v) {
+        packageLookup = v;
+        return this;
+    }
+
+    List<String> execute(Path root) throws IOException {
+        // Get the list of files in the root for which to look up for needed shared libraries
+        List<Path> allPackageFiles = Files.walk(root).filter(
+                Files::isRegularFile).filter(LibProvidersLookup::canDependOnLibs).collect(
+                Collectors.toList());
+
+        Collection<Path> neededLibs = getNeededLibsForFiles(allPackageFiles);
+
+        // Get the list of unique package names.
+        List<String> neededPackages = neededLibs.stream().map(libPath -> {
+            try {
+                List<String> packageNames = packageLookup.apply(libPath).filter(
+                        Objects::nonNull).filter(Predicate.not(String::isBlank)).distinct().collect(
+                        Collectors.toList());
+                Log.verbose(String.format("%s is provided by %s", libPath, packageNames));
+                return packageNames;
+            } catch (IOException ex) {
+                // Ignore and keep going
+                Log.verbose(ex);
+                List<String> packageNames = Collections.emptyList();
+                return packageNames;
+            }
+        }).flatMap(List::stream).sorted().distinct().collect(Collectors.toList());
+
+        return neededPackages;
+    }
+
+    private static List<Path> getNeededLibsForFile(Path path) throws IOException {
+        List<Path> result = new ArrayList<>();
+        int ret = Executor.of(TOOL_LDD, path.toString()).setOutputConsumer(lines -> {
+            lines.map(line -> {
+                Matcher matcher = LIB_IN_LDD_OUTPUT_REGEX.matcher(line);
+                if (matcher.find()) {
+                    return matcher.group(1);
+                }
+                return null;
+            }).filter(Objects::nonNull).map(Path::of).forEach(result::add);
+        }).execute();
+
+        if (ret != 0) {
+            // objdump failed. This is OK if the tool was applied to not a binary file
+            return Collections.emptyList();
+        }
+
+        return result;
+    }
+
+    private static Collection<Path> getNeededLibsForFiles(List<Path> paths) {
+        // Depending on tool used, the set can contain full paths (ldd) or
+        // only file names (objdump).
+        Set<Path> allLibs = paths.stream().map(path -> {
+            List<Path> libs;
+            try {
+                libs = getNeededLibsForFile(path);
+            } catch (IOException ex) {
+                Log.verbose(ex);
+                libs = Collections.emptyList();
+            }
+            return libs;
+        }).flatMap(List::stream).collect(Collectors.toSet());
+
+        // `allLibs` contains names of all .so needed by files from `paths` list.
+        // If there are mutual dependencies between binaries from `paths` list,
+        // then names or full paths to these binaries are in `allLibs` set.
+        // Remove these items from `allLibs`.
+        Set<Path> excludedNames = paths.stream().map(Path::getFileName).collect(
+                Collectors.toSet());
+        Iterator<Path> it = allLibs.iterator();
+        while (it.hasNext()) {
+            Path libName = it.next().getFileName();
+            if (excludedNames.contains(libName)) {
+                it.remove();
+            }
+        }
+
+        return allLibs;
+    }
+
+    private static boolean canDependOnLibs(Path path) {
+        return path.toFile().canExecute() || path.toString().endsWith(".so");
+    }
+
+    @FunctionalInterface
+    public interface PackageLookup {
+        Stream<String> apply(Path path) throws IOException;
+    }
+
+    private PackageLookup packageLookup;
+
+    private static final String TOOL_LDD = "ldd";
+
+    //
+    // Typical ldd output:
+    //
+    // ldd: warning: you do not have execution permission for `/tmp/jdk.jpackage17911687595930080396/images/opt/simplepackagetest/lib/runtime/lib/libawt_headless.so'
+    //  linux-vdso.so.1 =>  (0x00007ffce6bfd000)
+    //  libawt.so => /tmp/jdk.jpackage17911687595930080396/images/opt/simplepackagetest/lib/runtime/lib/libawt.so (0x00007f4e00c75000)
+    //  libjvm.so => not found
+    //  libjava.so => /tmp/jdk.jpackage17911687595930080396/images/opt/simplepackagetest/lib/runtime/lib/libjava.so (0x00007f4e00c41000)
+    //  libm.so.6 => /lib64/libm.so.6 (0x00007f4e00834000)
+    //  libdl.so.2 => /lib64/libdl.so.2 (0x00007f4e00630000)
+    //  libc.so.6 => /lib64/libc.so.6 (0x00007f4e00262000)
+    //  libjvm.so => not found
+    //  libjvm.so => not found
+    //  libverify.so => /tmp/jdk.jpackage17911687595930080396/images/opt/simplepackagetest/lib/runtime/lib/libverify.so (0x00007f4e00c2e000)
+    //  /lib64/ld-linux-x86-64.so.2 (0x00007f4e00b36000)
+    //  libjvm.so => not found
+    //
+    private static final Pattern LIB_IN_LDD_OUTPUT_REGEX = Pattern.compile(
+            "^\\s*\\S+\\s*=>\\s*(\\S+)\\s+\\(0[xX]\\p{XDigit}+\\)");
+}
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppBundler.java	Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppBundler.java	Mon Sep 30 19:33:13 2019 -0400
@@ -39,9 +39,6 @@
 
 public class LinuxAppBundler extends AbstractImageBundler {
 
-    private static final ResourceBundle I18N = ResourceBundle.getBundle(
-            "jdk.jpackage.internal.resources.LinuxResources");
-
     static final BundlerParamInfo<File> ICON_PNG =
             new StandardBundlerParam<>(
             "icon.png",
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppImageBuilder.java	Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppImageBuilder.java	Mon Sep 30 19:33:13 2019 -0400
@@ -42,9 +42,6 @@
 
 public class LinuxAppImageBuilder extends AbstractAppImageBuilder {
 
-    private static final ResourceBundle I18N = ResourceBundle.getBundle(
-        "jdk.jpackage.internal.resources.LinuxResources");
-
     private static final String LIBRARY_NAME = "libapplauncher.so";
     final static String DEFAULT_ICON = "java32.png";
 
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java	Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java	Mon Sep 30 19:33:13 2019 -0400
@@ -37,12 +37,14 @@
 import java.nio.file.attribute.PosixFilePermissions;
 import java.text.MessageFormat;
 import java.util.*;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import static jdk.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR;
 
 import static jdk.jpackage.internal.StandardBundlerParam.*;
-import static jdk.jpackage.internal.LinuxPackageBundler.I18N;
+
 
 public class LinuxDebBundler extends LinuxPackageBundler {
 
@@ -82,18 +84,29 @@
                 return s;
             });
 
+    private final static String TOOL_DPKG_DEB = "dpkg-deb";
+    private final static String TOOL_DPKG = "dpkg";
+    private final static String TOOL_FAKEROOT = "fakeroot";
+
+    private final static String DEB_ARCH;
+    static {
+        String debArch;
+        try {
+            debArch = Executor.of(TOOL_DPKG, "--print-architecture").saveOutput(
+                    true).executeExpectSuccess().getOutput().get(0);
+        } catch (IOException ex) {
+            debArch = null;
+        }
+        DEB_ARCH = debArch;
+    }
+
     private static final BundlerParamInfo<String> FULL_PACKAGE_NAME =
             new StandardBundlerParam<>(
                     "linux.deb.fullPackageName", String.class, params -> {
-                        try {
-                            return PACKAGE_NAME.fetchFrom(params)
+                        return PACKAGE_NAME.fetchFrom(params)
                             + "_" + VERSION.fetchFrom(params)
                             + "-" + RELEASE.fetchFrom(params)
-                            + "_" + getDebArch();
-                        } catch (IOException ex) {
-                            Log.verbose(ex);
-                            return null;
-                        }
+                            + "_" + DEB_ARCH;
                     }, (s, p) -> s);
 
     private static final BundlerParamInfo<String> EMAIL =
@@ -126,39 +139,17 @@
                 try {
                     String licenseFile = LICENSE_FILE.fetchFrom(params);
                     if (licenseFile != null) {
-                        StringBuilder contentBuilder = new StringBuilder();
-                        try (Stream<String> stream = Files.lines(Path.of(
-                                licenseFile), StandardCharsets.UTF_8)) {
-                            stream.forEach(s -> contentBuilder.append(s).append(
-                                    "\n"));
-                        }
-                        return contentBuilder.toString();
+                        return Files.lines(Path.of(licenseFile),
+                                StandardCharsets.UTF_8).collect(
+                                        Collectors.joining("\n"));
                     }
-                } catch (Exception e) {
+                } catch (IOException e) {
                     Log.verbose(e);
                 }
                 return "Unknown";
             },
             (s, p) -> s);
 
-    private final static String TOOL_DPKG_DEB = "dpkg-deb";
-    private final static String TOOL_DPKG = "dpkg";
-
-    public static boolean testTool(String toolName, String minVersion) {
-        try {
-            ProcessBuilder pb = new ProcessBuilder(
-                    toolName,
-                    "--version");
-            // not interested in the output
-            IOUtils.exec(pb, true, null);
-        } catch (Exception e) {
-            Log.verbose(MessageFormat.format(I18N.getString(
-                    "message.test-for-tool"), toolName, e.getMessage()));
-            return false;
-        }
-        return true;
-    }
-
     public LinuxDebBundler() {
         super(PACKAGE_NAME);
     }
@@ -166,28 +157,21 @@
     @Override
     public void doValidate(Map<String, ? super Object> params)
             throws ConfigException {
-        // NOTE: Can we validate that the required tools are available
-        // before we start?
-        if (!testTool(TOOL_DPKG_DEB, "1")){
-            throw new ConfigException(MessageFormat.format(
-                    I18N.getString("error.tool-not-found"), TOOL_DPKG_DEB),
-                    I18N.getString("error.tool-not-found.advice"));
-        }
-        if (!testTool(TOOL_DPKG, "1")){
-            throw new ConfigException(MessageFormat.format(
-                    I18N.getString("error.tool-not-found"), TOOL_DPKG),
-                    I18N.getString("error.tool-not-found.advice"));
-        }
 
-
-        // Show warning is license file is missing
-        String licenseFile = LICENSE_FILE.fetchFrom(params);
-        if (licenseFile == null) {
+        // Show warning if license file is missing
+        if (LICENSE_FILE.fetchFrom(params) == null) {
             Log.verbose(I18N.getString("message.debs-like-licenses"));
         }
     }
 
     @Override
+    protected List<ToolValidator> getToolValidators(
+            Map<String, ? super Object> params) {
+        return Stream.of(TOOL_DPKG_DEB, TOOL_DPKG, TOOL_FAKEROOT).map(
+                ToolValidator::new).collect(Collectors.toList());
+    }
+
+    @Override
     protected File buildPackageBundle(
             Map<String, String> replacementData,
             Map<String, ? super Object> params, File outputParentDir) throws
@@ -198,6 +182,83 @@
         return buildDeb(params, outputParentDir);
     }
 
+    private static final Pattern PACKAGE_NAME_REGEX = Pattern.compile("^(^\\S+):");
+
+    @Override
+    protected void initLibProvidersLookup(
+            Map<String, ? super Object> params,
+            LibProvidersLookup libProvidersLookup) {
+
+        //
+        // `dpkg -S` command does glob pattern lookup. If not the absolute path
+        // to the file is specified it might return mltiple package names.
+        // Even for full paths multiple package names can be returned as
+        // it is OK for multiple packages to provide the same file. `/opt`
+        // directory is such an example. So we have to deal with multiple
+        // packages per file situation.
+        //
+        // E.g.: `dpkg -S libc.so.6` command reports three packages:
+        // libc6-x32: /libx32/libc.so.6
+        // libc6:amd64: /lib/x86_64-linux-gnu/libc.so.6
+        // libc6-i386: /lib32/libc.so.6
+        // `:amd64` is architecture suffix and can (should) be dropped.
+        // Still need to decide what package to choose from three.
+        // libc6-x32 and libc6-i386 both depend on libc6:
+        // $ dpkg -s libc6-x32
+        // Package: libc6-x32
+        // Status: install ok installed
+        // Priority: optional
+        // Section: libs
+        // Installed-Size: 10840
+        // Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
+        // Architecture: amd64
+        // Source: glibc
+        // Version: 2.23-0ubuntu10
+        // Depends: libc6 (= 2.23-0ubuntu10)
+        //
+        // We can dive into tracking dependencies, but this would be overly
+        // complicated.
+        //
+        // For simplicity lets consider the following rules:
+        // 1. If there is one item in `dpkg -S` output, accept it.
+        // 2. If there are multiple items in `dpkg -S` output and there is at
+        //  least one item with the default arch suffix (DEB_ARCH),
+        //  accept only these items.
+        // 3. If there are multiple items in `dpkg -S` output and there are
+        //  no with the default arch suffix (DEB_ARCH), accept all items.
+        // So lets use this heuristics: don't accept packages for whom
+        //  `dpkg -p` command fails.
+        // 4. Arch suffix should be stripped from accepted package names.
+        //
+
+        libProvidersLookup.setPackageLookup(file -> {
+            Set<String> archPackages = new HashSet<>();
+            Set<String> otherPackages = new HashSet<>();
+
+            Executor.of(TOOL_DPKG, "-S", file.toString())
+                    .saveOutput(true).executeExpectSuccess()
+                    .getOutput().forEach(line -> {
+                        Matcher matcher = PACKAGE_NAME_REGEX.matcher(line);
+                        if (matcher.find()) {
+                            String name = matcher.group(1);
+                            if (name.endsWith(":" + DEB_ARCH)) {
+                                // Strip arch suffix
+                                name = name.substring(0,
+                                        name.length() - (DEB_ARCH.length() + 1));
+                                archPackages.add(name);
+                            } else {
+                                otherPackages.add(name);
+                            }
+                        }
+                    });
+
+            if (!archPackages.isEmpty()) {
+                return archPackages.stream();
+            }
+            return otherPackages.stream();
+        });
+    }
+
     /*
      * set permissions with a string like "rwxr-xr-x"
      *
@@ -217,23 +278,13 @@
 
     }
 
-    private static String getDebArch() throws IOException {
-        try (var baos = new ByteArrayOutputStream();
-                var ps = new PrintStream(baos)) {
-            var pb = new ProcessBuilder(TOOL_DPKG, "--print-architecture");
-            IOUtils.exec(pb, false, ps);
-            return baos.toString().split("\n", 2)[0];
-        }
-    }
-
     public static boolean isDebian() {
         // we are just going to run "dpkg -s coreutils" and assume Debian
         // or deritive if no error is returned.
-        var pb = new ProcessBuilder(TOOL_DPKG, "-s", "coreutils");
         try {
-            int ret = pb.start().waitFor();
-            return (ret == 0);
-        } catch (IOException | InterruptedException e) {
+            Executor.of(TOOL_DPKG, "-s", "coreutils").executeExpectSuccess();
+            return true;
+        } catch (IOException e) {
             // just fall thru
         }
         return false;
@@ -340,7 +391,7 @@
         data.put("APPLICATION_SECTION", SECTION.fetchFrom(params));
         data.put("APPLICATION_COPYRIGHT", COPYRIGHT.fetchFrom(params));
         data.put("APPLICATION_LICENSE_TEXT", LICENSE_TEXT.fetchFrom(params));
-        data.put("APPLICATION_ARCH", getDebArch());
+        data.put("APPLICATION_ARCH", DEB_ARCH);
         data.put("APPLICATION_INSTALLED_SIZE", Long.toString(
                 createMetaPackage(params).sourceApplicationLayout().sizeInBytes() >> 10));
 
@@ -363,12 +414,16 @@
 
         PlatformPackage thePackage = createMetaPackage(params);
 
+        List<String> cmdline = new ArrayList<>();
+        cmdline.addAll(List.of(TOOL_FAKEROOT, TOOL_DPKG_DEB));
+        if (Log.isVerbose()) {
+            cmdline.add("--verbose");
+        }
+        cmdline.addAll(List.of("-b", thePackage.sourceRoot().toString(),
+                outFile.getAbsolutePath()));
+
         // run dpkg
-        ProcessBuilder pb = new ProcessBuilder(
-                "fakeroot", TOOL_DPKG_DEB, "-b",
-                thePackage.sourceRoot().toString(),
-                outFile.getAbsolutePath());
-        IOUtils.exec(pb);
+        Executor.of(cmdline.toArray(String[]::new)).executeExpectSuccess();
 
         Log.verbose(MessageFormat.format(I18N.getString(
                 "message.output-to-location"), outFile.getAbsolutePath()));
@@ -388,17 +443,11 @@
 
     @Override
     public boolean supported(boolean runtimeInstaller) {
-        if (Platform.getPlatform() == Platform.LINUX) {
-            if (testTool(TOOL_DPKG_DEB, "1")) {
-                return true;
-            }
-        }
-        return false;
+        return Platform.isLinux() && (new ToolValidator(TOOL_DPKG_DEB).validate() == null);
     }
 
     @Override
     public boolean isDefault() {
         return isDebian();
     }
-
 }
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageBundler.java	Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageBundler.java	Mon Sep 30 19:33:13 2019 -0400
@@ -25,24 +25,12 @@
 package jdk.jpackage.internal;
 
 import java.awt.image.BufferedImage;
-import java.io.BufferedReader;
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Writer;
+import java.io.*;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.ResourceBundle;
+import java.util.*;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import javax.imageio.ImageIO;
@@ -58,9 +46,6 @@
 
 abstract class LinuxPackageBundler extends AbstractBundler {
 
-    protected static final ResourceBundle I18N = ResourceBundle.getBundle(
-            "jdk.jpackage.internal.resources.LinuxResources");
-
     private static final String DESKTOP_COMMANDS_INSTALL = "DESKTOP_COMMANDS_INSTALL";
     private static final String DESKTOP_COMMANDS_UNINSTALL = "DESKTOP_COMMANDS_UNINSTALL";
     private static final String UTILITY_SCRIPTS = "UTILITY_SCRIPTS";
@@ -95,37 +80,47 @@
     }
 
     private final BundlerParamInfo<String> packageName;
+    private boolean withFindNeededPackages;
 
     @Override
     final public boolean validate(Map<String, ? super Object> params)
             throws ConfigException {
-        try {
-            if (params == null) throw new ConfigException(
-                    I18N.getString("error.parameters-null"),
-                    I18N.getString("error.parameters-null.advice"));
 
-            // run basic validation to ensure requirements are met
-            // we are not interested in return code, only possible exception
-            APP_BUNDLER.fetchFrom(params).validate(params);
+        // run basic validation to ensure requirements are met
+        // we are not interested in return code, only possible exception
+        APP_BUNDLER.fetchFrom(params).validate(params);
 
-            validateFileAssociations(FILE_ASSOCIATIONS.fetchFrom(params));
+        validateFileAssociations(FILE_ASSOCIATIONS.fetchFrom(params));
 
-            // If package name has some restrictions, the string converter will
-            // throw an exception if invalid
-            packageName.getStringConverter().apply(packageName.fetchFrom(params),
-                params);
+        // If package name has some restrictions, the string converter will
+        // throw an exception if invalid
+        packageName.getStringConverter().apply(packageName.fetchFrom(params),
+            params);
 
-            // Packaging specific validation
-            doValidate(params);
-
-            return true;
-        } catch (RuntimeException re) {
-            if (re.getCause() instanceof ConfigException) {
-                throw (ConfigException) re.getCause();
-            } else {
-                throw new ConfigException(re);
+        for (var validator: getToolValidators(params)) {
+            ConfigException ex = validator.validate();
+            if (ex != null) {
+                throw ex;
             }
         }
+
+        withFindNeededPackages = LibProvidersLookup.supported();
+        if (!withFindNeededPackages) {
+            final String advice;
+            if ("deb".equals(getID())) {
+                advice = "message.deb-ldd-not-available.advice";
+            } else {
+                advice = "message.rpm-ldd-not-available.advice";
+            }
+            // Let user know package dependencies will not be generated.
+            Log.error(String.format("%s\n%s", I18N.getString(
+                    "message.ldd-not-available"), I18N.getString(advice)));
+        }
+
+        // Packaging specific validation
+        doValidate(params);
+
+        return true;
     }
 
     @Override
@@ -168,13 +163,18 @@
                 }
             }
 
+            if (!StandardBundlerParam.isRuntimeInstaller(params)) {
+                desktopIntegration = new DesktopIntegration(thePackage, params);
+            } else {
+                desktopIntegration = null;
+            }
+
             Map<String, String> data = createDefaultReplacementData(params);
-            if (StandardBundlerParam.isRuntimeInstaller(params)) {
+            if (desktopIntegration != null) {
+                data.putAll(desktopIntegration.create());
+            } else {
                 Stream.of(DESKTOP_COMMANDS_INSTALL, DESKTOP_COMMANDS_UNINSTALL,
                         UTILITY_SCRIPTS).forEach(v -> data.put(v, ""));
-            } else {
-                data.putAll(
-                        new DesktopIntegration(thePackage, params).prepareForApplication());
             }
 
             data.putAll(createReplacementData(params));
@@ -187,6 +187,39 @@
         }
     }
 
+    private List<String> getListOfNeededPackages(
+            Map<String, ? super Object> params) throws IOException {
+
+        PlatformPackage thePackage = createMetaPackage(params);
+
+        final List<String> xdgUtilsPackage;
+        if (desktopIntegration != null) {
+            xdgUtilsPackage = desktopIntegration.requiredPackages();
+        } else {
+            xdgUtilsPackage = Collections.emptyList();
+        }
+
+        final List<String> neededLibPackages;
+        if (withFindNeededPackages) {
+            LibProvidersLookup lookup = new LibProvidersLookup();
+            initLibProvidersLookup(params, lookup);
+
+            neededLibPackages = lookup.execute(thePackage.sourceRoot());
+        } else {
+            neededLibPackages = Collections.emptyList();
+        }
+
+        // Merge all package lists together.
+        // Filter out empty names, sort and remove duplicates.
+        List<String> result = Stream.of(xdgUtilsPackage, neededLibPackages).flatMap(
+                List::stream).filter(Predicate.not(String::isEmpty)).sorted().distinct().collect(
+                Collectors.toList());
+
+        Log.verbose(String.format("Required packages: %s", result));
+
+        return result;
+    }
+
     private Map<String, String> createDefaultReplacementData(
             Map<String, ? super Object> params) throws IOException {
         Map<String, String> data = new HashMap<>();
@@ -196,13 +229,26 @@
         data.put("APPLICATION_VERSION", VERSION.fetchFrom(params));
         data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params));
         data.put("APPLICATION_RELEASE", RELEASE.fetchFrom(params));
-        data.put("PACKAGE_DEPENDENCIES", LINUX_PACKAGE_DEPENDENCIES.fetchFrom(
-                params));
+
+        String defaultDeps = String.join(", ", getListOfNeededPackages(params));
+        String customDeps = LINUX_PACKAGE_DEPENDENCIES.fetchFrom(params).strip();
+        if (!customDeps.isEmpty() && !defaultDeps.isEmpty()) {
+            customDeps = ", " + customDeps;
+        }
+        data.put("PACKAGE_DEFAULT_DEPENDENCIES", defaultDeps);
+        data.put("PACKAGE_CUSTOM_DEPENDENCIES", customDeps);
 
         return data;
     }
 
-    abstract void doValidate(Map<String, ? super Object> params)
+    abstract protected void initLibProvidersLookup(
+            Map<String, ? super Object> params,
+            LibProvidersLookup libProvidersLookup);
+
+    abstract protected List<ToolValidator> getToolValidators(
+            Map<String, ? super Object> params);
+
+    abstract protected void doValidate(Map<String, ? super Object> params)
             throws ConfigException;
 
     abstract protected Map<String, String> createReplacementData(
@@ -333,11 +379,21 @@
                 iconFile = null;
             }
 
-            this.desktopFileData = Collections.unmodifiableMap(
+            desktopFileData = Collections.unmodifiableMap(
                     createDataForDesktopFile(params));
+
+            nestedIntegrations = launchers.stream().map(
+                    launcherParams -> new DesktopIntegration(thePackage,
+                            launcherParams)).collect(Collectors.toList());
         }
 
-        Map<String, String> prepareForApplication() throws IOException {
+        List<String> requiredPackages() {
+            return Stream.of(List.of(this), nestedIntegrations).flatMap(
+                    List::stream).map(DesktopIntegration::requiredPackagesSelf).flatMap(
+                    List::stream).distinct().collect(Collectors.toList());
+        }
+
+        Map<String, String> create() throws IOException {
             if (iconFile != null) {
                 // Create application icon file.
                 prepareSrcIconFile();
@@ -386,15 +442,12 @@
                     data.get(DESKTOP_COMMANDS_INSTALL)));
             List<String> uninstallShellCmds = new ArrayList<>(Arrays.asList(
                     data.get(DESKTOP_COMMANDS_UNINSTALL)));
-            for (Map<String, ? super Object> params : launchers) {
-                DesktopIntegration integration = new DesktopIntegration(
-                        thePackage, params);
-
+            for (var integration: nestedIntegrations) {
                 if (!integration.associations.isEmpty()) {
                     needCleanupScripts = true;
                 }
 
-                Map<String, String> launcherData = integration.prepareForApplication();
+                Map<String, String> launcherData = integration.create();
 
                 installShellCmds.add(launcherData.get(DESKTOP_COMMANDS_INSTALL));
                 uninstallShellCmds.add(launcherData.get(
@@ -421,6 +474,13 @@
             return data;
         }
 
+        private List<String> requiredPackagesSelf() {
+            if (desktopFile != null) {
+                return List.of("xdg-utils");
+            }
+            return Collections.emptyList();
+        }
+
         private Map<String, String> createDataForDesktopFile(
                 Map<String, ? super Object> params) {
             Map<String, String> data = new HashMap<>();
@@ -558,6 +618,8 @@
         private final DesktopFile desktopFile;
         private final DesktopFile iconFile;
 
+        final private List<DesktopIntegration> nestedIntegrations;
+
         private final Map<String, String> desktopFileData;
 
         /**
@@ -709,4 +771,6 @@
         return String.join(System.lineSeparator(), commands.stream().filter(
                 s -> s != null && !s.isEmpty()).collect(Collectors.toList()));
     }
+
+    private DesktopIntegration desktopIntegration;
 }
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmBundler.java	Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmBundler.java	Mon Sep 30 19:33:13 2019 -0400
@@ -32,6 +32,7 @@
 import java.util.*;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
 import static jdk.jpackage.internal.StandardBundlerParam.*;
 import static jdk.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR;
@@ -103,32 +104,9 @@
 
     private final static String DEFAULT_SPEC_TEMPLATE = "template.spec";
 
+    public final static String TOOL_RPM = "rpm";
     public final static String TOOL_RPMBUILD = "rpmbuild";
-    public final static double TOOL_RPMBUILD_MIN_VERSION = 4.0d;
-
-    public static boolean testTool(String toolName, double minVersion) {
-        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
-                PrintStream ps = new PrintStream(baos)) {
-            ProcessBuilder pb = new ProcessBuilder(toolName, "--version");
-            IOUtils.exec(pb, false, ps);
-                    //not interested in the above's output
-            String content = new String(baos.toByteArray());
-            Pattern pattern = Pattern.compile(" (\\d+\\.\\d+)");
-            Matcher matcher = pattern.matcher(content);
-
-            if (matcher.find()) {
-                String v = matcher.group(1);
-                double version = Double.parseDouble(v);
-                return minVersion <= version;
-            } else {
-               return false;
-            }
-        } catch (Exception e) {
-            Log.verbose(MessageFormat.format(I18N.getString(
-                    "message.test-for-tool"), toolName, e.getMessage()));
-            return false;
-        }
-    }
+    public final static String TOOL_RPMBUILD_MIN_VERSION = "4.0";
 
     public LinuxRpmBundler() {
         super(PACKAGE_NAME);
@@ -137,20 +115,26 @@
     @Override
     public void doValidate(Map<String, ? super Object> params)
             throws ConfigException {
-        if (params == null) throw new ConfigException(
-                I18N.getString("error.parameters-null"),
-                I18N.getString("error.parameters-null.advice"));
+    }
 
-        // validate presense of required tools
-        if (!testTool(TOOL_RPMBUILD, TOOL_RPMBUILD_MIN_VERSION)){
-            throw new ConfigException(
-                MessageFormat.format(
-                    I18N.getString("error.cannot-find-rpmbuild"),
-                    TOOL_RPMBUILD_MIN_VERSION),
-                MessageFormat.format(
-                    I18N.getString("error.cannot-find-rpmbuild.advice"),
-                    TOOL_RPMBUILD_MIN_VERSION));
-        }
+    private static ToolValidator createRpmbuildToolValidator() {
+        Pattern pattern = Pattern.compile(" (\\d+\\.\\d+)");
+        return new ToolValidator(TOOL_RPMBUILD).setMinimalVersion(
+                TOOL_RPMBUILD_MIN_VERSION).setVersionParser(lines -> {
+                    String versionString = lines.limit(1).collect(
+                            Collectors.toList()).get(0);
+                    Matcher matcher = pattern.matcher(versionString);
+                    if (matcher.find()) {
+                        return matcher.group(1);
+                    }
+                    return null;
+                });
+    }
+
+    @Override
+    protected List<ToolValidator> getToolValidators(
+            Map<String, ? super Object> params) {
+        return List.of(createRpmbuildToolValidator());
     }
 
     @Override
@@ -193,6 +177,18 @@
         return data;
     }
 
+    @Override
+    protected void initLibProvidersLookup(
+            Map<String, ? super Object> params,
+            LibProvidersLookup libProvidersLookup) {
+        libProvidersLookup.setPackageLookup(file -> {
+            return Executor.of(TOOL_RPM,
+                "-q", "--queryformat", "%{name}\\n",
+                "-q", "--whatprovides", file.toString())
+                .saveOutput(true).executeExpectSuccess().getOutput().stream();
+        });
+    }
+
     private Path specFile(Map<String, ? super Object> params) {
         return TEMP_ROOT.fetchFrom(params).toPath().resolve(Path.of("SPECS",
                 PACKAGE_NAME.fetchFrom(params) + ".spec"));
@@ -207,17 +203,18 @@
         PlatformPackage thePackage = createMetaPackage(params);
 
         //run rpmbuild
-        ProcessBuilder pb = new ProcessBuilder(
+        Executor.of(
                 TOOL_RPMBUILD,
                 "-bb", specFile(params).toAbsolutePath().toString(),
-                "--define", String.format("%%_sourcedir %s", thePackage.sourceRoot()),
+                "--define", String.format("%%_sourcedir %s",
+                        thePackage.sourceRoot()),
                 // save result to output dir
-                "--define", String.format("%%_rpmdir %s", outdir.getAbsolutePath()),
+                "--define", String.format("%%_rpmdir %s",
+                        outdir.getAbsolutePath()),
                 // do not use other system directories to build as current user
                 "--define", String.format("%%_topdir %s",
                         TEMP_ROOT.fetchFrom(params).toPath().toAbsolutePath())
-        );
-        IOUtils.exec(pb);
+        ).executeExpectSuccess();
 
         Log.verbose(MessageFormat.format(
                 I18N.getString("message.output-bundle-location"),
@@ -253,12 +250,7 @@
 
     @Override
     public boolean supported(boolean runtimeInstaller) {
-        if (Platform.getPlatform() == Platform.LINUX) {
-            if (testTool(TOOL_RPMBUILD, TOOL_RPMBUILD_MIN_VERSION)) {
-                return true;
-            }
-        }
-        return false;
+        return Platform.isLinux() && (createRpmbuildToolValidator().validate() == null);
     }
 
     @Override
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/LinuxResources.properties	Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/LinuxResources.properties	Mon Sep 30 19:33:13 2019 -0400
@@ -42,14 +42,14 @@
 
 error.parameters-null=Parameters map is null
 error.parameters-null.advice=Pass in a non-null parameters map
-error.tool-not-found=Can not find {0}
+
 error.tool-not-found.advice=Please install required packages
+error.tool-old-version.advice=Please install required packages
+
 error.no-content-types-for-file-association=No MIME types were specified for File Association number {0}
 error.too-many-content-types-for-file-association=More than one MIME types was specified for File Association number {0}.
 error.invalid-value-for-package-name=Invalid value "{0}" for the bundle name.
 error.invalid-value-for-package-name.advice=Set the "linux-bundle-name" option to a valid Debian package name. Note that the package names must consist only of lower case letters (a-z), digits (0-9), plus (+) and minus (-) signs, and periods (.). They must be at least two characters long and must start with an alphanumeric character.
-error.cannot-find-rpmbuild=Can not find rpmbuild {0} or newer
-error.cannot-find-rpmbuild.advice=\  Install packages needed to build RPM, version {0} or newer
 
 message.icon-not-png=The specified icon "{0}" is not a PNG file and will not be used. The default icon will be used in it's place.
 message.test-for-tool=Test for [{0}]. Result: {1}
@@ -59,3 +59,7 @@
 message.outputting-bundle-location=Generating RPM for installer to: {0}.
 message.output-bundle-location=Package (.rpm) saved to: {0}.
 message.creating-association-with-null-extension=Creating association with null extension.
+
+message.ldd-not-available=ldd command not found. Package dependencies will not be generated.
+message.deb-ldd-not-available.advice=Install "libc-bin" DEB package to get ldd.
+message.rpm-ldd-not-available.advice=Install "glibc-common" RPM package to get ldd.
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/LinuxResources_ja.properties	Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/LinuxResources_ja.properties	Mon Sep 30 19:33:13 2019 -0400
@@ -42,14 +42,14 @@
 
 error.parameters-null=Parameters map is null
 error.parameters-null.advice=Pass in a non-null parameters map
-error.tool-not-found=Can not find {0}
+
 error.tool-not-found.advice=Please install required packages
+error.tool-old-version.advice=Please install required packages
+
 error.no-content-types-for-file-association=No MIME types were specified for File Association number {0}
 error.too-many-content-types-for-file-association=More than one MIME types was specified for File Association number {0}.
 error.invalid-value-for-package-name=Invalid value "{0}" for the bundle name.
 error.invalid-value-for-package-name.advice=Set the "linux-bundle-name" option to a valid Debian package name. Note that the package names must consist only of lower case letters (a-z), digits (0-9), plus (+) and minus (-) signs, and periods (.). They must be at least two characters long and must start with an alphanumeric character.
-error.cannot-find-rpmbuild=Can not find rpmbuild {0} or newer
-error.cannot-find-rpmbuild.advice=\  Install packages needed to build RPM, version {0} or newer
 
 message.icon-not-png=The specified icon "{0}" is not a PNG file and will not be used. The default icon will be used in it's place.
 message.test-for-tool=Test for [{0}]. Result: {1}
@@ -59,3 +59,7 @@
 message.outputting-bundle-location=Generating RPM for installer to: {0}.
 message.output-bundle-location=Package (.rpm) saved to: {0}.
 message.creating-association-with-null-extension=Creating association with null extension.
+
+message.ldd-not-available=ldd command not found. Package dependencies will not be generated.
+message.deb-ldd-not-available.advice=Install "libc-bin" DEB package to get ldd.
+message.rpm-ldd-not-available.advice=Install "glibc-common" RPM package to get ldd.
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/LinuxResources_zh_CN.properties	Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/LinuxResources_zh_CN.properties	Mon Sep 30 19:33:13 2019 -0400
@@ -42,14 +42,14 @@
 
 error.parameters-null=Parameters map is null
 error.parameters-null.advice=Pass in a non-null parameters map
-error.tool-not-found=Can not find {0}
+
 error.tool-not-found.advice=Please install required packages
+error.tool-old-version.advice=Please install required packages
+
 error.no-content-types-for-file-association=No MIME types were specified for File Association number {0}
 error.too-many-content-types-for-file-association=More than one MIME types was specified for File Association number {0}.
 error.invalid-value-for-package-name=Invalid value "{0}" for the bundle name.
 error.invalid-value-for-package-name.advice=Set the "linux-bundle-name" option to a valid Debian package name. Note that the package names must consist only of lower case letters (a-z), digits (0-9), plus (+) and minus (-) signs, and periods (.). They must be at least two characters long and must start with an alphanumeric character.
-error.cannot-find-rpmbuild=Can not find rpmbuild {0} or newer
-error.cannot-find-rpmbuild.advice=\  Install packages needed to build RPM, version {0} or newer
 
 message.icon-not-png=The specified icon "{0}" is not a PNG file and will not be used. The default icon will be used in it's place.
 message.test-for-tool=Test for [{0}]. Result: {1}
@@ -59,3 +59,7 @@
 message.outputting-bundle-location=Generating RPM for installer to: {0}.
 message.output-bundle-location=Package (.rpm) saved to: {0}.
 message.creating-association-with-null-extension=Creating association with null extension.
+
+message.ldd-not-available=ldd command not found. Package dependencies will not be generated.
+message.deb-ldd-not-available.advice=Install "libc-bin" DEB package to get ldd.
+message.rpm-ldd-not-available.advice=Install "glibc-common" RPM package to get ldd.
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.control	Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.control	Mon Sep 30 19:33:13 2019 -0400
@@ -6,5 +6,5 @@
 Architecture: APPLICATION_ARCH
 Provides: APPLICATION_PACKAGE
 Description: APPLICATION_DESCRIPTION
-Depends: PACKAGE_DEPENDENCIES
+Depends: PACKAGE_DEFAULT_DEPENDENCIES PACKAGE_CUSTOM_DEPENDENCIES
 Installed-Size: APPLICATION_INSTALLED_SIZE
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.spec	Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.spec	Mon Sep 30 19:33:13 2019 -0400
@@ -12,8 +12,8 @@
 
 Autoprov: 0
 Autoreq: 0
-%if "xPACKAGE_DEPENDENCIES" != x
-Requires: PACKAGE_DEPENDENCIES
+%if "xPACKAGE_DEFAULT_DEPENDENCIES" != x || "xPACKAGE_CUSTOM_DEPENDENCIES" != x
+Requires: PACKAGE_DEFAULT_DEPENDENCIES PACKAGE_CUSTOM_DEPENDENCIES
 %endif
 
 #avoid ARCH subfolder
--- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ConfigException.java	Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ConfigException.java	Mon Sep 30 19:33:13 2019 -0400
@@ -34,6 +34,11 @@
         this.advice = advice;
     }
 
+    public ConfigException(String msg, String advice, Exception cause) {
+        super(msg, cause);
+        this.advice = advice;
+    }
+
     public ConfigException(Exception cause) {
         super(cause);
         this.advice = null;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Executor.java	Mon Sep 30 19:33:13 2019 -0400
@@ -0,0 +1,162 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. 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.
+ */
+package jdk.jpackage.internal;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+final public class Executor {
+
+    Executor() {
+    }
+
+    Executor setOutputConsumer(Consumer<Stream<String>> v) {
+        outputConsumer = v;
+        return this;
+    }
+
+    Executor saveOutput(boolean v) {
+        saveOutput = v;
+        return this;
+    }
+
+    Executor setProcessBuilder(ProcessBuilder v) {
+        pb = v;
+        return this;
+    }
+
+    Executor setCommandLine(String... cmdline) {
+        return setProcessBuilder(new ProcessBuilder(cmdline));
+    }
+
+    List<String> getOutput() {
+        return output;
+    }
+
+    Executor executeExpectSuccess() throws IOException {
+        int ret = execute();
+        if (0 != ret) {
+            throw new IOException(
+                    String.format("Command %s exited with %d code",
+                            createLogMessage(pb), ret));
+        }
+        return this;
+    }
+
+    int execute() throws IOException {
+        output = null;
+
+        boolean needProcessOutput = outputConsumer != null || Log.isVerbose() || saveOutput;
+        if (needProcessOutput) {
+            pb.redirectErrorStream(true);
+        } else {
+            // We are not going to read process output, so need to notify
+            // ProcessBuilder about this. Otherwise some processes might just
+            // hang up (`ldconfig -p`).
+            pb.redirectError(ProcessBuilder.Redirect.DISCARD);
+            pb.redirectOutput(ProcessBuilder.Redirect.DISCARD);
+        }
+
+        Log.verbose(String.format("Running %s", createLogMessage(pb)));
+        Process p = pb.start();
+
+        if (needProcessOutput) {
+            try (var br = new BufferedReader(new InputStreamReader(
+                    p.getInputStream()))) {
+                final List<String> savedOutput;
+                // Need to save output if explicitely requested (saveOutput=true) or
+                // if will be used used by multiple consumers
+                if ((outputConsumer != null && Log.isVerbose()) || saveOutput) {
+                    savedOutput = br.lines().collect(Collectors.toList());
+                    if (saveOutput) {
+                        output = savedOutput;
+                    }
+                } else {
+                    savedOutput = null;
+                }
+
+                Supplier<Stream<String>> outputStream = () -> {
+                    if (savedOutput != null) {
+                        return savedOutput.stream();
+                    }
+                    return br.lines();
+                };
+
+                if (Log.isVerbose()) {
+                    outputStream.get().forEach(Log::verbose);
+                }
+
+                if (outputConsumer != null) {
+                    outputConsumer.accept(outputStream.get());
+                }
+
+                if (savedOutput == null) {
+                    // For some processes on Linux if the output stream
+                    // of the process is opened but not consumed, the process
+                    // would exit with code 141.
+                    // It turned out that reading just a single line of process
+                    // output fixes the problem, but let's process
+                    // all of the output, just in case.
+                    br.lines().forEach(x -> {});
+                }
+            }
+        }
+
+        try {
+            return p.waitFor();
+        } catch (InterruptedException ex) {
+            Log.verbose(ex);
+            throw new RuntimeException(ex);
+        }
+    }
+
+    static Executor of(String... cmdline) {
+        return new Executor().setCommandLine(cmdline);
+    }
+
+    static Executor of(ProcessBuilder pb) {
+        return new Executor().setProcessBuilder(pb);
+    }
+
+    private static String createLogMessage(ProcessBuilder pb) {
+        StringBuilder sb = new StringBuilder();
+        sb.append(String.format("%s", pb.command()));
+        if (pb.directory() != null) {
+            sb.append(String.format("in %s", pb.directory().getAbsolutePath()));
+        }
+        return sb.toString();
+    }
+
+    private ProcessBuilder pb;
+    private boolean saveOutput;
+    private List<String> output;
+    private Consumer<Stream<String>> outputConsumer;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/I18N.java	Mon Sep 30 19:33:13 2019 -0400
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. 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.
+ */
+package jdk.jpackage.internal;
+
+import java.util.ResourceBundle;
+
+class I18N {
+
+    static String getString(String key) {
+        if (PLATFORM.containsKey(key)) {
+            return PLATFORM.getString(key);
+        }
+        return SHARED.getString(key);
+    }
+
+    private static final ResourceBundle SHARED = ResourceBundle.getBundle(
+            "jdk.jpackage.internal.resources.MainResources");
+
+    private static final ResourceBundle PLATFORM;
+
+    static {
+        if (Platform.isLinux()) {
+            PLATFORM = ResourceBundle.getBundle(
+                    "jdk.jpackage.internal.resources.LinuxResources");
+        } else if (Platform.isWindows()) {
+            PLATFORM = ResourceBundle.getBundle(
+                    "jdk.jpackage.internal.resources.WinResources");
+        } else if (Platform.isMac()) {
+            PLATFORM = ResourceBundle.getBundle(
+                    "jdk.jpackage.internal.resources.MacResources");
+        } else {
+            throw new IllegalStateException("Unknwon platform");
+        }
+    }
+}
--- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/IOUtils.java	Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/IOUtils.java	Mon Sep 30 19:33:13 2019 -0400
@@ -26,8 +26,6 @@
 package jdk.jpackage.internal;
 
 import java.io.*;
-import java.net.URL;
-import java.util.Arrays;
 import java.nio.channels.FileChannel;
 import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
@@ -150,37 +148,20 @@
         exec(pb, false, null);
     }
 
-    public static void exec(ProcessBuilder pb, boolean testForPresenseOnly,
+    static void exec(ProcessBuilder pb, boolean testForPresenseOnly,
             PrintStream consumer) throws IOException {
-        pb.redirectErrorStream(true);
-        Log.verbose("Running "
-                + Arrays.toString(pb.command().toArray(new String[0]))
-                + (pb.directory() != null ? (" in " + pb.directory()) : ""));
-        Process p = pb.start();
-        InputStreamReader isr = new InputStreamReader(p.getInputStream());
-        BufferedReader br = new BufferedReader(isr);
-        String lineRead;
-        String output = "";
-        while ((lineRead = br.readLine()) != null) {
+        List<String> output = new ArrayList<>();
+        Executor exec = Executor.of(pb).setOutputConsumer(lines -> {
+            lines.forEach(output::add);
             if (consumer != null) {
-                consumer.print(lineRead + '\n');
-            } else {
-               Log.verbose(lineRead);
+                output.forEach(consumer::println);
             }
-            output += lineRead;
-        }
-        try {
-            int ret = p.waitFor();
-            if (ret != 0 && !(testForPresenseOnly && ret != 127)) {
-                throw new IOException("Exec failed with code "
-                        + ret + " command ["
-                        + Arrays.toString(pb.command().toArray(new String[0]))
-                        + " in " + (pb.directory() != null ?
-                                pb.directory().getAbsolutePath() :
-                                "unspecified directory")
-                        + " output: " + output);
-            }
-        } catch (InterruptedException ex) {
+        });
+
+        if (testForPresenseOnly) {
+            exec.execute();
+        } else {
+            exec.executeExpectSuccess();
         }
     }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ToolValidator.java	Mon Sep 30 19:33:13 2019 -0400
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. 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.
+ */
+package jdk.jpackage.internal;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+
+public final class ToolValidator {
+
+    ToolValidator(String name) {
+        this.name = name;
+        args = new ArrayList<>();
+
+        if (Platform.getPlatform() == Platform.LINUX) {
+            setCommandLine("--version");
+        }
+    }
+
+    ToolValidator setCommandLine(String... args) {
+        this.args = List.of(args);
+        return this;
+    }
+
+    ToolValidator setMinimalVersion(String v) {
+        minimalVersion = v;
+        return this;
+    }
+
+    ToolValidator setVersionParser(Function<Stream<String>, String> v) {
+        versionParser = v;
+        return this;
+    }
+
+    ConfigException validate() {
+        List<String> cmdline = new ArrayList<>();
+        cmdline.add(name);
+        cmdline.addAll(args);
+        try {
+            ProcessBuilder pb = new ProcessBuilder(cmdline);
+            AtomicBoolean canUseTool = new AtomicBoolean();
+            if (minimalVersion == null) {
+                // No version check.
+                canUseTool.setPlain(true);
+            }
+
+            Executor.of(pb).setOutputConsumer(lines -> {
+                if (versionParser != null && minimalVersion != null) {
+                    String version = versionParser.apply(lines);
+                    if (minimalVersion.compareTo(version) < 0) {
+                        canUseTool.setPlain(true);
+                    }
+                }
+            }).execute();
+
+            if (!canUseTool.getPlain()) {
+                return new ConfigException(MessageFormat.format(I18N.getString(
+                        "error.tool-old-version"), name, minimalVersion),
+                        MessageFormat.format(I18N.getString(
+                                "error.tool-old-version.advice"), name,
+                                minimalVersion));
+            }
+        } catch (IOException e) {
+            return new ConfigException(MessageFormat.format(I18N.getString(
+                    "error.tool-not-found"), name, e.getMessage()),
+                    MessageFormat.format(I18N.getString(
+                            "error.tool-not-found.advice"), name), e);
+        }
+
+        // All good. Tool can be used.
+        return null;
+    }
+
+    private final String name;
+    private List<String> args;
+    private String minimalVersion;
+    private Function<Stream<String>, String> versionParser;
+}
--- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties	Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties	Mon Sep 30 19:33:13 2019 -0400
@@ -53,6 +53,11 @@
 error.main-jar-does-not-exist=The configured main jar does not exist {0} in the input directory
 error.main-jar-does-not-exist.advice=The main jar must be specified relative to the input directory (not an absolute path), and must exist within that directory
 
+error.tool-not-found=Can not find {0}. Reason: {1}
+error.tool-not-found.advice=Please install {0}
+error.tool-old-version=Can not find {0} {1} or newer
+error.tool-old-version.advice=Please install {0} {1} or newer
+
 warning.module.does.not.exist=Module [{0}] does not exist
 warning.no.jdk.modules.found=Warning: No JDK Modules found
 
--- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources_ja.properties	Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources_ja.properties	Mon Sep 30 19:33:13 2019 -0400
@@ -53,6 +53,11 @@
 error.main-jar-does-not-exist=The configured main jar does not exist {0} in the input directory
 error.main-jar-does-not-exist.advice=The main jar must be specified relative to the input directory (not an absolute path), and must exist within that directory
 
+error.tool-not-found=Can not find {0}. Reason: {1}
+error.tool-not-found.advice=Please install {0}
+error.tool-old-version=Can not find {0} {1} or newer
+error.tool-old-version.advice=Please install {0} {1} or newer
+
 warning.module.does.not.exist=Module [{0}] does not exist
 warning.no.jdk.modules.found=Warning: No JDK Modules found
 
--- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources_zh_CN.properties	Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources_zh_CN.properties	Mon Sep 30 19:33:13 2019 -0400
@@ -53,6 +53,11 @@
 error.main-jar-does-not-exist=The configured main jar does not exist {0} in the input directory
 error.main-jar-does-not-exist.advice=The main jar must be specified relative to the input directory (not an absolute path), and must exist within that directory
 
+error.tool-not-found=Can not find {0}. Reason: {1}
+error.tool-not-found.advice=Please install {0}
+error.tool-old-version=Can not find {0} {1} or newer
+error.tool-old-version.advice=Please install {0} {1} or newer
+
 warning.module.does.not.exist=Module [{0}] does not exist
 warning.no.jdk.modules.found=Warning: No JDK Modules found
 
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java	Mon Sep 30 19:11:19 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java	Mon Sep 30 19:33:13 2019 -0400
@@ -216,6 +216,10 @@
         } else if (saveOutputType.contains(SaveOutputType.DUMP)) {
             builder.inheritIO();
             sb.append("; inherit I/O");
+        } else {
+            builder.redirectError(ProcessBuilder.Redirect.DISCARD);
+            builder.redirectOutput(ProcessBuilder.Redirect.DISCARD);
+            sb.append("; discard I/O");
         }
         if (directory != null) {
             builder.directory(directory.toFile());
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java	Mon Sep 30 19:11:19 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java	Mon Sep 30 19:33:13 2019 -0400
@@ -191,7 +191,66 @@
                 .executeAndGetFirstLineOfOutput();
     }
 
-    static void addDebBundleDesktopIntegrationVerifier(PackageTest test,
+    static void verifyPackageBundleEssential(JPackageCommand cmd) {
+        String packageName = LinuxHelper.getPackageName(cmd);
+        TKit.assertNotEquals(0L, LinuxHelper.getInstalledPackageSizeKB(
+                cmd), String.format(
+                        "Check installed size of [%s] package in KB is not zero",
+                        packageName));
+
+        final boolean checkPrerequisites;
+        if (cmd.isRuntime()) {
+            Path runtimeDir = cmd.appRuntimeInstallationDirectory();
+            Set<Path> expectedCriticalRuntimePaths = CRITICAL_RUNTIME_FILES.stream().map(
+                    runtimeDir::resolve).collect(Collectors.toSet());
+            Set<Path> actualCriticalRuntimePaths = getPackageFiles(cmd).filter(
+                    expectedCriticalRuntimePaths::contains).collect(
+                            Collectors.toSet());
+            checkPrerequisites = expectedCriticalRuntimePaths.equals(
+                    actualCriticalRuntimePaths);
+        } else {
+            checkPrerequisites = true;
+        }
+
+        List<String> prerequisites = LinuxHelper.getPrerequisitePackages(cmd);
+        if (checkPrerequisites) {
+            final String vitalPackage = "libc";
+            TKit.assertTrue(prerequisites.stream().filter(
+                    dep -> dep.contains(vitalPackage)).findAny().isPresent(),
+                    String.format(
+                            "Check [%s] package is in the list of required packages %s of [%s] package",
+                            vitalPackage, prerequisites, packageName));
+        } else {
+            TKit.trace(String.format(
+                    "Not cheking %s required packages of [%s] package",
+                    prerequisites, packageName));
+        }
+    }
+
+    static void addBundleDesktopIntegrationVerifier(PackageTest test,
+            boolean integrated) {
+        final String xdgUtils = "xdg-utils";
+
+        test.addBundleVerifier(cmd -> {
+            List<String> prerequisites = getPrerequisitePackages(cmd);
+            boolean xdgUtilsFound = prerequisites.contains(xdgUtils);
+            if (integrated) {
+                TKit.assertTrue(xdgUtilsFound, String.format(
+                        "Check [%s] is in the list of required packages %s",
+                        xdgUtils, prerequisites));
+            } else {
+                TKit.assertFalse(xdgUtilsFound, String.format(
+                        "Check [%s] is NOT in the list of required packages %s",
+                        xdgUtils, prerequisites));
+            }
+        });
+
+        test.forTypes(PackageType.LINUX_DEB, () -> {
+            addDebBundleDesktopIntegrationVerifier(test, integrated);
+        });
+    }
+
+    private static void addDebBundleDesktopIntegrationVerifier(PackageTest test,
             boolean integrated) {
         Function<List<String>, String> verifier = (lines) -> {
             // Lookup for xdg commands
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java	Mon Sep 30 19:11:19 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java	Mon Sep 30 19:33:13 2019 -0400
@@ -129,8 +129,8 @@
     }
 
     public PackageTest addBundleDesktopIntegrationVerifier(boolean integrated) {
-        forTypes(LINUX_DEB, () -> {
-            LinuxHelper.addDebBundleDesktopIntegrationVerifier(this, integrated);
+        forTypes(LINUX, () -> {
+            LinuxHelper.addBundleDesktopIntegrationVerifier(this, integrated);
         });
         return this;
     }
@@ -322,10 +322,7 @@
         private void verifyPackageBundle(JPackageCommand cmd,
                 Executor.Result result) {
             if (PackageType.LINUX.contains(cmd.packageType())) {
-                TKit.assertNotEquals(0L, LinuxHelper.getInstalledPackageSizeKB(
-                        cmd), String.format(
-                                "Check installed size of [%s] package in KB is not zero",
-                                LinuxHelper.getPackageName(cmd)));
+                LinuxHelper.verifyPackageBundleEssential(cmd);
             }
             bundleVerifiers.stream().forEach(v -> v.accept(cmd, result));
         }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jpackage/junit/jdk/jpackage/internal/ToolValidatorTest.java	Mon Sep 30 19:33:13 2019 -0400
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. 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.
+ */
+package jdk.jpackage.internal;
+
+import java.nio.file.Path;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assert.*;
+import org.junit.Test;
+
+
+public class ToolValidatorTest {
+
+    @Test
+    public void testAvailable() {
+        assertNull(new ToolValidator(TOOL_JAVA).validate());
+    }
+
+    @Test
+    public void testNotAvailable() {
+        assertValidationFailure(new ToolValidator(TOOL_UNKNOWN).validate(), true);
+    }
+
+    @Test
+    public void testVersionParserUsage() {
+        // Without minimal version configured, version parser should not be used
+        new ToolValidator(TOOL_JAVA).setVersionParser(unused -> {
+            throw new RuntimeException();
+        }).validate();
+
+        // Minimal version is 1, actual is 10. Should be OK.
+        assertNull(
+                new ToolValidator(TOOL_JAVA).setMinimalVersion(
+                        "1").setVersionParser(unused -> "10").validate());
+
+        // Minimal version is 5, actual is 4.99.37. Error expected.
+        assertValidationFailure(new ToolValidator(TOOL_JAVA).setMinimalVersion(
+                "5").setVersionParser(unused -> "4.99.37").validate(), false);
+    }
+
+    private static void assertValidationFailure(ConfigException v,
+            boolean withCause) {
+        assertNotNull(v);
+        assertThat("", is(not(v.getMessage().strip())));
+        assertThat("", is(not(v.advice.strip())));
+        if (withCause) {
+            assertNotNull(v.getCause());
+        } else {
+            assertNull(v.getCause());
+        }
+    }
+
+    private final static String TOOL_JAVA;
+    private final static String TOOL_UNKNOWN = Path.of(System.getProperty(
+            "java.home"), "bin").toString();
+
+    static {
+        String fname = "java";
+        if (Platform.isWindows()) {
+            fname = fname + ".exe";
+        }
+        TOOL_JAVA = Path.of(System.getProperty("java.home"), "bin", fname).toString();
+    }
+}
--- a/test/jdk/tools/jpackage/junit/junit.java	Mon Sep 30 19:11:19 2019 -0400
+++ b/test/jdk/tools/jpackage/junit/junit.java	Mon Sep 30 19:33:13 2019 -0400
@@ -31,5 +31,6 @@
  *  jdk.jpackage.internal.PathGroupTest
  *  jdk.jpackage.internal.DeployParamsTest
  *  jdk.jpackage.internal.ApplicationLayoutTest
+ *  jdk.jpackage.internal.ToolValidatorTest
  *  jdk.jpackage.internal.AppImageFileTest
  */
--- a/test/jdk/tools/jpackage/linux/PackageDepsTest.java	Mon Sep 30 19:11:19 2019 -0400
+++ b/test/jdk/tools/jpackage/linux/PackageDepsTest.java	Mon Sep 30 19:33:13 2019 -0400
@@ -24,6 +24,7 @@
 import jdk.jpackage.test.TKit;
 import jdk.jpackage.test.PackageTest;
 import jdk.jpackage.test.PackageType;
+import jdk.jpackage.test.LinuxHelper;
 
 
 /**
@@ -74,10 +75,14 @@
             .addInitializer(cmd -> {
                 cmd.addArguments("--linux-package-deps", PREREQ_PACKAGE_NAME);
             })
-            .forTypes(PackageType.LINUX_DEB)
-            .addBundlePropertyVerifier("Depends", PREREQ_PACKAGE_NAME)
-            .forTypes(PackageType.LINUX_RPM)
-            .addBundlePropertyVerifier("Requires", PREREQ_PACKAGE_NAME)
+            .forTypes(PackageType.LINUX)
+            .addBundleVerifier(cmd -> {
+                TKit.assertTrue(
+                        LinuxHelper.getPrerequisitePackages(cmd).contains(
+                                PREREQ_PACKAGE_NAME), String.format(
+                                "Check package depends on [%s] package",
+                                PREREQ_PACKAGE_NAME));
+            })
             .run();
         });
     }