8231277 : Adjust Linux application image layout JDK-8200758-branch
authorherrick
Tue, 24 Sep 2019 13:43:58 -0400
branchJDK-8200758-branch
changeset 58302 718bd56695b3
parent 58301 e0efb29609bd
child 58303 88453b906981
8231277 : Adjust Linux application image layout Submitted-by: asemenyuk Reviewed-by: herrick, almatvee
src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppImageBuilder.java
src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageBundler.java
src/jdk.jpackage/linux/native/jpackageapplauncher/launcher.cpp
src/jdk.jpackage/linux/native/libapplauncher/LinuxPlatform.cpp
src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacAppImageBuilder.java
src/jdk.jpackage/share/classes/jdk/jpackage/internal/AbstractAppImageBuilder.java
src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationLayout.java
src/jdk.jpackage/share/classes/jdk/jpackage/internal/JLinkBundlerHelper.java
src/jdk.jpackage/share/classes/jdk/jpackage/internal/PathGroup.java
src/jdk.jpackage/share/classes/jdk/jpackage/internal/StandardBundlerParam.java
src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WindowsAppImageBuilder.java
test/jdk/tools/jpackage/helpers/JPackageInstallerHelper.java
test/jdk/tools/jpackage/helpers/JPackagePath.java
test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java
test/jdk/tools/jpackage/jdk/jpackage/internal/DeployParamsTest.java
test/jdk/tools/jpackage/junit/jdk/jpackage/internal/ApplicationLayoutTest.java
test/jdk/tools/jpackage/junit/jdk/jpackage/internal/DeployParamsTest.java
test/jdk/tools/jpackage/junit/jdk/jpackage/internal/PathGroupTest.java
test/jdk/tools/jpackage/junit/junit.java
test/jdk/tools/jpackage/junit/run_junit.sh
test/jdk/tools/jpackage/linux/BundleNameTest.java
test/jdk/tools/jpackage/linux/LinuxBundleNameTest.java
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppImageBuilder.java	Tue Sep 24 13:41:16 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppImageBuilder.java	Tue Sep 24 13:43:58 2019 -0400
@@ -48,12 +48,7 @@
     private static final String LIBRARY_NAME = "libapplauncher.so";
     final static String DEFAULT_ICON = "java32.png";
 
-    private final Path root;
-    private final Path appDir;
-    private final Path appModsDir;
-    private final Path runtimeDir;
-    private final Path binDir;
-    private final Path mdir;
+    private final ApplicationLayout appLayout;
 
     public static final BundlerParamInfo<File> ICON_PNG =
             new StandardBundlerParam<>(
@@ -70,35 +65,17 @@
             },
             (s, p) -> new File(s));
 
+    private static ApplicationLayout createAppLayout(Map<String, Object> params,
+            Path imageOutDir) {
+        return ApplicationLayout.linuxApp().resolveAt(
+                imageOutDir.resolve(APP_NAME.fetchFrom(params)));
+    }
+
     public LinuxAppImageBuilder(Map<String, Object> params, Path imageOutDir)
             throws IOException {
-        super(params,
-                imageOutDir.resolve(APP_NAME.fetchFrom(params) + "/runtime"));
-
-        Objects.requireNonNull(imageOutDir);
+        super(params, createAppLayout(params, imageOutDir).runtimeDirectory());
 
-        this.root = imageOutDir.resolve(APP_NAME.fetchFrom(params));
-        this.appDir = root.resolve("app");
-        this.appModsDir = appDir.resolve("mods");
-        this.runtimeDir = root.resolve("runtime");
-        this.binDir = root.resolve("bin");
-        this.mdir = runtimeDir.resolve("lib");
-        Files.createDirectories(appDir);
-        Files.createDirectories(runtimeDir);
-    }
-
-    public LinuxAppImageBuilder(String appName, Path imageOutDir)
-            throws IOException {
-        super(null, imageOutDir.resolve(appName));
-
-        Objects.requireNonNull(imageOutDir);
-
-        this.root = imageOutDir.resolve(appName);
-        this.appDir = null;
-        this.appModsDir = null;
-        this.runtimeDir = null;
-        this.binDir = null;
-        this.mdir = null;
+        appLayout = createAppLayout(params, imageOutDir);
     }
 
     private void writeEntry(InputStream in, Path dstFile) throws IOException {
@@ -110,19 +87,31 @@
         return APP_NAME.fetchFrom(params);
     }
 
-    public static String getLauncherCfgName(
-            Map<String, ? super Object> params) {
-        return "app" + File.separator + APP_NAME.fetchFrom(params) + ".cfg";
+    private Path getLauncherCfgPath(Map<String, ? super Object> params) {
+        return appLayout.appDirectory().resolve(
+                APP_NAME.fetchFrom(params) + ".cfg");
     }
 
     @Override
     public Path getAppDir() {
-        return appDir;
+        return appLayout.appDirectory();
     }
 
     @Override
     public Path getAppModsDir() {
-        return appModsDir;
+        return appLayout.appModsDirectory();
+    }
+
+    @Override
+    protected String getCfgAppDir() {
+        return Path.of("$APPDIR").resolve(
+                ApplicationLayout.linuxApp().appDirectory()).toString() + File.separator;
+    }
+
+    @Override
+    protected String getCfgRuntimeDir() {
+        return Path.of("$APPDIR").resolve(
+                ApplicationLayout.linuxApp().runtimeDirectory()).toString();
     }
 
     @Override
@@ -130,19 +119,20 @@
             throws IOException {
         Map<String, ? super Object> originalParams = new HashMap<>(params);
 
-        try {
-            IOUtils.writableOutputDir(root);
-            IOUtils.writableOutputDir(binDir);
-        } catch (PackagerException pe) {
-            throw new RuntimeException(pe);
-        }
+        appLayout.roots().stream().forEach(dir -> {
+            try {
+                IOUtils.writableOutputDir(dir);
+            } catch (PackagerException pe) {
+                throw new RuntimeException(pe);
+            }
+        });
 
         // create the primary launcher
         createLauncherForEntryPoint(params);
 
         // Copy library to the launcher folder
         try (InputStream is_lib = getResourceAsStream(LIBRARY_NAME)) {
-            writeEntry(is_lib, binDir.resolve(LIBRARY_NAME));
+            writeEntry(is_lib, appLayout.dllDirectory().resolve(LIBRARY_NAME));
         }
 
         // create the additional launchers, if any
@@ -166,8 +156,8 @@
 
     private void createLauncherForEntryPoint(
             Map<String, ? super Object> params) throws IOException {
-        // Copy executable to Linux folder
-        Path executableFile = binDir.resolve(getLauncherName(params));
+        // Copy executable to launchers folder
+        Path executableFile = appLayout.launchersDirectory().resolve(getLauncherName(params));
         try (InputStream is_launcher =
                 getResourceAsStream("jpackageapplauncher")) {
             writeEntry(is_launcher, executableFile);
@@ -176,14 +166,15 @@
         executableFile.toFile().setExecutable(true, false);
         executableFile.toFile().setWritable(true, true);
 
-        writeCfgFile(params, root.resolve(getLauncherCfgName(params)).toFile());
+        writeCfgFile(params, getLauncherCfgPath(params).toFile());
     }
 
     private void copyIcon(Map<String, ? super Object> params)
             throws IOException {
 
         File icon = ICON_PNG.fetchFrom(params);
-        File iconTarget = binDir.resolve(APP_NAME.fetchFrom(params) + ".png").toFile();
+        File iconTarget = appLayout.destktopIntegrationDirectory().resolve(
+                APP_NAME.fetchFrom(params) + ".png").toFile();
 
         InputStream in = locateResource(
                 iconTarget.getName(),
@@ -205,7 +196,7 @@
             }
             File srcdir = appResources.getBaseDirectory();
             for (String fname : appResources.getIncludedFiles()) {
-                copyEntry(appDir, srcdir, fname);
+                copyEntry(appLayout.appDirectory(), srcdir, fname);
             }
         }
     }
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageBundler.java	Tue Sep 24 13:41:16 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageBundler.java	Tue Sep 24 13:43:58 2019 -0400
@@ -254,7 +254,7 @@
         if (StandardBundlerParam.isRuntimeInstaller(params)) {
             return ApplicationLayout.javaRuntime();
         }
-        return ApplicationLayout.unixApp();
+        return ApplicationLayout.linuxApp();
     }
 
     private static void validateFileAssociations(
--- a/src/jdk.jpackage/linux/native/jpackageapplauncher/launcher.cpp	Tue Sep 24 13:41:16 2019 -0400
+++ b/src/jdk.jpackage/linux/native/jpackageapplauncher/launcher.cpp	Tue Sep 24 13:43:58 2019 -0400
@@ -57,7 +57,7 @@
     {
         std::string programPath = GetProgramPath();
         std::string libraryName = dirname((char*)programPath.c_str());
-        libraryName += "/libapplauncher.so";
+        libraryName += "/../lib/libapplauncher.so";
         library = dlopen(libraryName.c_str(), RTLD_LAZY);
 
         if (library == NULL) {
--- a/src/jdk.jpackage/linux/native/libapplauncher/LinuxPlatform.cpp	Tue Sep 24 13:41:16 2019 -0400
+++ b/src/jdk.jpackage/linux/native/libapplauncher/LinuxPlatform.cpp	Tue Sep 24 13:43:58 2019 -0400
@@ -66,7 +66,7 @@
 
 TString LinuxPlatform::GetPackageAppDirectory() {
     return FilePath::IncludeTrailingSeparator(
-            GetPackageRootDirectory()) + _T("app");
+            GetPackageRootDirectory()) + _T("lib/app");
 }
 
 TString LinuxPlatform::GetAppName() {
--- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacAppImageBuilder.java	Tue Sep 24 13:41:16 2019 -0400
+++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacAppImageBuilder.java	Tue Sep 24 13:43:58 2019 -0400
@@ -183,25 +183,6 @@
         Files.createDirectories(runtimeDir);
     }
 
-    public MacAppImageBuilder(Map<String, Object> params, String jreName,
-            Path imageOutDir) throws IOException {
-        super(null, imageOutDir.resolve(jreName + "/Contents/Home"));
-
-        Objects.requireNonNull(imageOutDir);
-
-        this.root = imageOutDir.resolve(jreName );
-        this.contentsDir = root.resolve("Contents");
-        this.javaDir = null;
-        this.javaModsDir = null;
-        this.resourcesDir = null;
-        this.macOSDir = null;
-        this.runtimeDir = this.root;
-        this.runtimeRoot = runtimeDir.resolve("Contents/Home");
-        this.mdir = runtimeRoot.resolve("lib");
-
-        Files.createDirectories(runtimeDir);
-    }
-
     private void writeEntry(InputStream in, Path dstFile) throws IOException {
         Files.createDirectories(dstFile.getParent());
         Files.copy(in, dstFile);
--- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AbstractAppImageBuilder.java	Tue Sep 24 13:41:16 2019 -0400
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AbstractAppImageBuilder.java	Tue Sep 24 13:43:58 2019 -0400
@@ -71,7 +71,7 @@
     public abstract Path getAppDir();
     public abstract Path getAppModsDir();
 
-    public Path getRoot() {
+    public Path getRuntimeRoot() {
         return this.root;
     }
 
@@ -175,6 +175,7 @@
 
     public void writeCfgFile(Map<String, ? super Object> params,
             File cfgFileName) throws IOException {
+        cfgFileName.getParentFile().mkdirs();
         cfgFileName.delete();
         File mainJar = JLinkBundlerHelper.getMainJar(params);
         ModFile.ModType mainJarType = ModFile.ModType.Unknown;
@@ -247,12 +248,12 @@
         return runtimeImageTop;
     }
 
-    String getCfgAppDir() {
+    protected String getCfgAppDir() {
         return "$APPDIR" + File.separator
                 + getAppDir().getFileName() + File.separator;
     }
 
-    String getCfgRuntimeDir() {
+    protected String getCfgRuntimeDir() {
         return "$APPDIR" + File.separator + "runtime";
     }
 
--- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationLayout.java	Tue Sep 24 13:41:16 2019 -0400
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationLayout.java	Tue Sep 24 13:43:58 2019 -0400
@@ -33,7 +33,7 @@
  */
 final class ApplicationLayout implements PathGroup.Facade<ApplicationLayout> {
     enum PathRole {
-        RUNTIME, APP, LAUNCHERS_DIR, DESKTOP
+        RUNTIME, APP, LAUNCHERS, DESKTOP, APP_MODS, DLLS
     }
 
     ApplicationLayout(Map<Object, Path> paths) {
@@ -58,7 +58,14 @@
      * Path to launchers directory.
      */
     Path launchersDirectory() {
-        return pathGroup().getPath(PathRole.LAUNCHERS_DIR);
+        return pathGroup().getPath(PathRole.LAUNCHERS);
+    }
+
+    /**
+     * Path to directory with dynamic libraries.
+     */
+    Path dllDirectory() {
+        return pathGroup().getPath(PathRole.DLLS);
     }
 
     /**
@@ -76,24 +83,33 @@
     }
 
     /**
+     * Path to application mods directory.
+     */
+    Path appModsDirectory() {
+        return pathGroup().getPath(PathRole.APP_MODS);
+    }
+
+    /**
      * Path to directory with application's desktop integration files.
      */
     Path destktopIntegrationDirectory() {
         return pathGroup().getPath(PathRole.DESKTOP);
     }
 
-    static ApplicationLayout unixApp() {
+    static ApplicationLayout linuxApp() {
         return new ApplicationLayout(Map.of(
-                PathRole.LAUNCHERS_DIR, Path.of("bin"),
-                PathRole.APP, Path.of("app"),
-                PathRole.RUNTIME, Path.of("runtime"),
-                PathRole.DESKTOP, Path.of("bin")
+                PathRole.LAUNCHERS, Path.of("bin"),
+                PathRole.APP, Path.of("lib/app"),
+                PathRole.RUNTIME, Path.of("lib/runtime"),
+                PathRole.DESKTOP, Path.of("lib"),
+                PathRole.DLLS, Path.of("lib"),
+                PathRole.APP_MODS, Path.of("lib/app/mods")
         ));
     }
 
     static ApplicationLayout windowsApp() {
         return new ApplicationLayout(Map.of(
-                PathRole.LAUNCHERS_DIR, Path.of(""),
+                PathRole.LAUNCHERS, Path.of(""),
                 PathRole.APP, Path.of("app"),
                 PathRole.RUNTIME, Path.of("runtime"),
                 PathRole.DESKTOP, Path.of("")
@@ -105,7 +121,7 @@
             return windowsApp();
         }
 
-        return unixApp();
+        return linuxApp();
     }
 
     static ApplicationLayout javaRuntime() {
--- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/JLinkBundlerHelper.java	Tue Sep 24 13:41:16 2019 -0400
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/JLinkBundlerHelper.java	Tue Sep 24 13:43:58 2019 -0400
@@ -150,7 +150,7 @@
                 StandardBundlerParam.ADD_MODULES.fetchFrom(params);
         Set<String> limitModules =
                 StandardBundlerParam.LIMIT_MODULES.fetchFrom(params);
-        Path outputDir = imageBuilder.getRoot();
+        Path outputDir = imageBuilder.getRuntimeRoot();
         File mainJar = getMainJar(params);
         ModFile.ModType mainJarType = ModFile.ModType.Unknown;
 
--- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PathGroup.java	Tue Sep 24 13:41:16 2019 -0400
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PathGroup.java	Tue Sep 24 13:43:58 2019 -0400
@@ -24,16 +24,18 @@
  */
 package jdk.jpackage.internal;
 
+import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.function.BiFunction;
 import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
 
 /**
@@ -52,8 +54,8 @@
     /**
      * All configured entries.
      */
-    Collection<Path> paths() {
-        return entries.values();
+    List<Path> paths() {
+        return entries.values().stream().collect(Collectors.toList());
     }
 
     /**
@@ -61,14 +63,19 @@
      */
     List<Path> roots() {
         // Sort by the number of path components in ascending order.
-        List<Path> sorted = paths().stream().sorted(
-                (a, b) -> a.getNameCount() - b.getNameCount()).collect(
+        List<Map.Entry<Path, Path>> sorted = normalizedPaths().stream().sorted(
+                (a, b) -> a.getKey().getNameCount() - b.getKey().getNameCount()).collect(
                         Collectors.toList());
 
-        return paths().stream().filter(
+        // Returns `true` if `a` is a parent of `b`
+        BiFunction<Map.Entry<Path, Path>, Map.Entry<Path, Path>, Boolean> isParentOrSelf = (a, b) -> {
+            return a == b || b.getKey().startsWith(a.getKey());
+        };
+
+        return sorted.stream().filter(
                 v -> v == sorted.stream().sequential().filter(
-                        v2 -> v == v2 || v2.endsWith(v)).findFirst().get()).collect(
-                        Collectors.toList());
+                        v2 -> isParentOrSelf.apply(v2, v)).findFirst().get()).map(
+                        v -> v.getValue()).collect(Collectors.toList());
     }
 
     long sizeInBytes() throws IOException {
@@ -132,49 +139,64 @@
     private static void copy(boolean move, List<Map.Entry<Path, Path>> entries)
             throws IOException {
 
-        // Reorder entries. Entries with source entries with the least amount of
-        // descending entries found between source entries should go first.
-        entries.sort((e1, e2) -> e1.getKey().getNameCount() - e2.getKey().getNameCount());
+        // destination -> source file mapping
+        Map<Path, Path> actions = new HashMap<>();
+        for (var action: entries) {
+            Path src = action.getKey();
+            Path dst = action.getValue();
+            if (src.toFile().isDirectory()) {
+                Files.walk(src).forEach(path -> actions.put(dst.resolve(
+                        src.relativize(path)).toAbsolutePath().normalize(), path));
+            } else {
+                actions.put(dst.toAbsolutePath().normalize(), src);
+            }
+        }
 
-        for (var entry : entries.stream().sequential().filter(e -> {
-            return e == entries.stream().sequential().filter(e2 -> isDuplicate(e2, e)).findFirst().get();
-                }).collect(Collectors.toList())) {
-            Path src = entry.getKey();
-            Path dst = entry.getValue();
+        for (var action : actions.entrySet()) {
+            Path dst = action.getKey();
+            Path src = action.getValue();
 
-            if (src.equals(dst)) {
+            if (src.equals(dst) || !src.toFile().exists()) {
                 continue;
             }
 
-            Files.createDirectories(dst.getParent());
-            if (move) {
-                Files.move(src, dst);
-            } else if (src.toFile().isDirectory()) {
-                IOUtils.copyRecursive(src, dst);
+            if (src.toFile().isDirectory()) {
+                Files.createDirectories(dst);
             } else {
-                IOUtils.copyFile(src.toFile(), dst.toFile());
+                Files.createDirectories(dst.getParent());
+                if (move) {
+                    Files.move(src, dst);
+                } else {
+                    Files.copy(src, dst);
+                }
+            }
+        }
+
+        if (move) {
+            // Delete source dirs.
+            for (var entry: entries) {
+                File srcFile = entry.getKey().toFile();
+                if (srcFile.isDirectory()) {
+                    IOUtils.deleteRecursive(srcFile);
+                }
             }
         }
     }
 
-    private static boolean isDuplicate(Map.Entry<Path, Path> a,
-            Map.Entry<Path, Path> b) {
-        if (a == b || a.equals(b)) {
-            return true;
+    private static Map.Entry<Path, Path> normalizedPath(Path v) {
+        final Path normalized;
+        if (!v.isAbsolute()) {
+            normalized = Path.of("./").resolve(v.normalize());
+        } else {
+            normalized = v.normalize();
         }
 
-        if (b.getKey().getNameCount() < a.getKey().getNameCount()) {
-            return isDuplicate(b, a);
-        }
+        return Map.entry(normalized, v);
+    }
 
-        if (!a.getKey().endsWith(b.getKey())) {
-            return false;
-        }
-
-        Path relativeSrcPath = a.getKey().relativize(b.getKey());
-        Path relativeDstPath = a.getValue().relativize(b.getValue());
-
-        return relativeSrcPath.equals(relativeDstPath);
+    private List<Map.Entry<Path, Path>> normalizedPaths() {
+        return entries.values().stream().map(PathGroup::normalizedPath).collect(
+                Collectors.toList());
     }
 
     private final Map<Object, Path> entries;
--- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/StandardBundlerParam.java	Tue Sep 24 13:41:16 2019 -0400
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/StandardBundlerParam.java	Tue Sep 24 13:43:58 2019 -0400
@@ -526,7 +526,7 @@
         File image = appBuilder.getRuntimeImageDir(topImage);
         // copy whole runtime, need to skip jmods and src.zip
         final List<String> excludes = Arrays.asList("jmods", "src.zip");
-        IOUtils.copyRecursive(image.toPath(), appBuilder.getRoot(), excludes);
+        IOUtils.copyRecursive(image.toPath(), appBuilder.getRuntimeRoot(), excludes);
 
         // if module-path given - copy modules to appDir/mods
         List<Path> modulePath =
--- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WindowsAppImageBuilder.java	Tue Sep 24 13:41:16 2019 -0400
+++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WindowsAppImageBuilder.java	Tue Sep 24 13:43:58 2019 -0400
@@ -127,21 +127,6 @@
         Files.createDirectories(runtimeDir);
     }
 
-    public WindowsAppImageBuilder(String jreName, Path imageOutDir)
-            throws IOException {
-        super(null, imageOutDir.resolve(jreName));
-
-        Objects.requireNonNull(imageOutDir);
-
-        this.root = imageOutDir.resolve(jreName);
-        this.appDir = null;
-        this.appModsDir = null;
-        this.runtimeDir = root;
-        this.mdir = runtimeDir.resolve("lib");
-        this.binDir = null;
-        Files.createDirectories(runtimeDir);
-    }
-
     private void writeEntry(InputStream in, Path dstFile) throws IOException {
         Files.createDirectories(dstFile.getParent());
         Files.copy(in, dstFile);
--- a/test/jdk/tools/jpackage/helpers/JPackageInstallerHelper.java	Tue Sep 24 13:41:16 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/JPackageInstallerHelper.java	Tue Sep 24 13:43:58 2019 -0400
@@ -144,100 +144,4 @@
             }
         }
     }
-
-    public static void validateStartMenu(String menuGroup, String menu, boolean exist)
-            throws Exception {
-        String startMenuLink = JPackagePath.getWinStartMenu() +
-                File.separator + menuGroup + File.separator +
-                menu + ".lnk";
-
-        File link = new File(startMenuLink);
-        if (exist) {
-            if (!link.exists()) {
-                throw new AssertionError("Cannot find " + link.getAbsolutePath());
-            }
-        } else {
-            if (link.exists()) {
-                throw new AssertionError("Error: " + link.getAbsolutePath() + " exist");
-            }
-        }
-    }
-
-    public static void validateDesktopShortcut(String name, boolean exist)
-            throws Exception {
-        String shortcutLink = JPackagePath.getWinPublicDesktop() +
-                File.separator + name + ".lnk";
-
-        File link = new File(shortcutLink);
-        if (exist) {
-            if (!link.exists()) {
-                throw new AssertionError("Cannot find " + link.getAbsolutePath());
-            }
-        } else {
-            if (link.exists()) {
-                throw new AssertionError("Error: " + link.getAbsolutePath() + " exist");
-            }
-        }
-    }
-
-    public static void validateUserLocalStartMenu(String menuGroup, String menu, boolean exist)
-            throws Exception {
-        String startMenuLink = JPackagePath.getWinUserLocalStartMenu() +
-                File.separator + menuGroup + File.separator +
-                menu + ".lnk";
-
-        File link = new File(startMenuLink);
-        if (exist) {
-            if (!link.exists()) {
-                throw new AssertionError("Cannot find " + link.getAbsolutePath());
-            }
-        } else {
-            if (link.exists()) {
-                throw new AssertionError("Error: " + link.getAbsolutePath() + " exist");
-            }
-        }
-    }
-
-    public static void validateWinRegistry(String key, String [] values, boolean retValZero)
-            throws Exception {
-        File outFile = new File("regOutput.txt");
-        if (outFile.exists()) {
-            outFile.delete();
-        }
-
-        int retVal = JPackageHelper.execute(outFile, "reg.exe", "query", key);
-        if (retValZero) {
-            if (retVal != 0) {
-                System.out.println("validateWinRegistry() key=" + key);
-                if (outFile.exists()) {
-                    System.err.println(Files.readString(outFile.toPath()));
-                }
-                throw new AssertionError(
-                       "Reg.exe application exited with error: " + retVal);
-            }
-        } else {
-            if (retVal == 0) {
-                System.out.println("validateWinRegistry() key=" + key);
-                if (outFile.exists()) {
-                    System.err.println(Files.readString(outFile.toPath()));
-                }
-                throw new AssertionError(
-                       "Reg.exe application exited without error: " + retVal);
-            } else {
-                return; // Done
-            }
-        }
-
-        if (!outFile.exists()) {
-            throw new AssertionError(outFile.getAbsolutePath() + " was not created");
-        }
-
-        String output = Files.readString(outFile.toPath());
-        for (String value : values) {
-            if (!output.contains(value)) {
-                System.err.println(output);
-                throw new AssertionError("Cannot find in registry: " + value);
-            }
-        }
-    }
 }
--- a/test/jdk/tools/jpackage/helpers/JPackagePath.java	Tue Sep 24 13:41:16 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/JPackagePath.java	Tue Sep 24 13:43:58 2019 -0400
@@ -30,18 +30,6 @@
  */
 public class JPackagePath {
 
-    // Path to Windows "Program Files" folder
-    // Probably better to figure this out programattically
-    private static final String WIN_PROGRAM_FILES = "C:\\Program Files";
-
-    // Path to Windows Start menu items
-    private static final String WIN_START_MENU =
-            "C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs";
-
-    // Path to Windows public desktop location
-    private static final String WIN_PUBLIC_DESKTOP =
-            "C:\\Users\\Public\\Desktop";
-
     // Return path to test src adjusted to location of caller
     public static String getTestSrcRoot() {
         return JPackageHelper.TEST_SRC_ROOT;
@@ -58,16 +46,7 @@
     }
 
     public static String getApp(String name) {
-        if (JPackageHelper.isWindows()) {
-            return Path.of("output", name, name + ".exe").toString();
-        } else if (JPackageHelper.isOSX()) {
-            return Path.of("output", name + ".app",
-                    "Contents", "MacOS", name).toString();
-        } else if (JPackageHelper.isLinux()) {
-            return Path.of("output", name, "bin", name).toString();
-        } else {
-            throw new AssertionError("Cannot detect platform");
-        }
+        return getAppSL(name, name);
     }
 
     // Returns path to generate test application icon
@@ -82,7 +61,7 @@
             return Path.of("output", name + ".app",
                     "Contents", "Resources", name + ".icns").toString();
         } else if (JPackageHelper.isLinux()) {
-            return Path.of("output", name, "bin", name + ".png").toString();
+            return Path.of("output", name, "lib", name + ".png").toString();
         } else {
             throw new AssertionError("Cannot detect platform");
         }
@@ -118,7 +97,7 @@
             return Path.of("output", name + ".app",
                     "Contents", "Java", name + ".cfg").toString();
         } else if (JPackageHelper.isLinux()) {
-            return Path.of("output", name, "app", name + ".cfg").toString();
+            return Path.of("output", name, "lib", "app", name + ".cfg").toString();
         } else {
             throw new AssertionError("Cannot detect platform");
         }
@@ -131,17 +110,9 @@
 
     public static String getRuntimeJava(String name) {
         if (JPackageHelper.isWindows()) {
-            return Path.of("output", name,
-                    "runtime", "bin", "java.exe").toString();
-        } else if (JPackageHelper.isOSX()) {
-            return Path.of("output", name + ".app", "Contents",
-                    "runtime", "Contents", "Home", "bin", "java").toString();
-        } else if (JPackageHelper.isLinux()) {
-            return Path.of("output", name,
-                    "runtime", "bin", "java").toString();
-        } else {
-            throw new AssertionError("Cannot detect platform");
+            return Path.of(getRuntimeBin(name), "java.exe").toString();
         }
+        return Path.of(getRuntimeBin(name), "java").toString();
     }
 
     // Returns output file name generate by test application
@@ -162,45 +133,12 @@
                     "Contents", "runtime",
                     "Contents", "Home", "bin").toString();
         } else if (JPackageHelper.isLinux()) {
-            return Path.of("output", name, "runtime", "bin").toString();
+            return Path.of("output", name, "lib", "runtime", "bin").toString();
         } else {
             throw new AssertionError("Cannot detect platform");
         }
     }
 
-    public static String getWinProgramFiles() {
-        return WIN_PROGRAM_FILES;
-    }
-
-    public static String getWinUserLocal() {
-        return Path.of(System.getProperty("user.home"),
-                "AppData", "Local").toString();
-    }
-
-    public static String getWinStartMenu() {
-        return WIN_START_MENU;
-    }
-
-    public static String getWinPublicDesktop() {
-        return WIN_PUBLIC_DESKTOP;
-    }
-
-    public static String getWinUserLocalStartMenu() {
-        return Path.of(System.getProperty("user.home"), "AppData", "Roaming",
-                "Microsoft", "Windows", "Start Menu", "Programs").toString();
-    }
-
-    public static String getWinInstalledApp(String testName) {
-        return Path.of(getWinProgramFiles(), testName,
-                testName + ".exe").toString();
-    }
-
-    public static String getWinInstalledApp(String installDir,
-            String testName) {
-        return Path.of(getWinProgramFiles(), installDir,
-                testName + ".exe").toString();
-    }
-
     public static String getOSXInstalledApp(String testName) {
         return File.separator + "Applications"
                 + File.separator + testName + ".app"
@@ -209,10 +147,6 @@
                 + File.separator + testName;
     }
 
-    public static String getLinuxInstalledApp(String testName) {
-        return Path.of("/opt", testName, "bin", testName).toString();
-    }
-
     public static String getOSXInstalledApp(String subDir, String testName) {
         return File.separator + "Applications"
                 + File.separator + subDir
@@ -222,41 +156,6 @@
                 + File.separator + testName;
     }
 
-    public static String getLinuxInstalledApp(String subDir, String testName) {
-        return Path.of("/opt", subDir, testName, "bin", testName).toString();
-    }
-
-    public static String getWinInstallFolder(String testName) {
-        return getWinProgramFiles()
-                + File.separator + testName;
-    }
-
-    public static String getLinuxInstallFolder(String testName) {
-        return File.separator + "opt"
-                + File.separator + testName;
-    }
-
-    public static String getLinuxInstallFolder(String subDir, String testName) {
-        if (testName == null) {
-            return File.separator + "opt"
-                    + File.separator + subDir;
-        } else {
-            return File.separator + "opt"
-                    + File.separator + subDir
-                    + File.separator + testName;
-        }
-    }
-
-    public static String getWinUserLocalInstalledApp(String testName) {
-        return getWinUserLocal()
-                + File.separator + testName
-                + File.separator + testName + ".exe";
-    }
-
-    public static String getWinUserLocalInstallFolder(String testName) {
-        return getWinUserLocal() + File.separator + testName;
-    }
-
     // Returs path to test license file
     public static String getLicenseFilePath() {
         String path = JPackagePath.getTestSrcRoot()
@@ -265,11 +164,4 @@
 
         return path;
     }
-
-    // Returns path to app folder of installed application
-    public static String getWinInstalledAppFolder(String testName) {
-        return getWinProgramFiles()
-                + File.separator + testName
-                + File.separator + "app";
-    }
 }
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java	Tue Sep 24 13:41:16 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java	Tue Sep 24 13:43:58 2019 -0400
@@ -307,7 +307,7 @@
         if (PackageType.IMAGE == packageType()) {
             return null;
         }
-        return appInstallationDirectory().resolve("runtime");
+        return appInstallationDirectory().resolve(appRuntimePath(packageType()));
     }
 
     /**
@@ -388,8 +388,6 @@
     /**
      * Returns path to runtime directory relative to image directory.
      *
-     * Function will always return "runtime".
-     *
      * @throws IllegalArgumentException if command is configured for platform
      * packaging
      */
@@ -399,6 +397,13 @@
             throw new IllegalArgumentException("Unexpected package type");
         }
 
+        return appRuntimePath(type);
+    }
+
+    private static Path appRuntimePath(PackageType type) {
+        if (PackageType.LINUX.contains(type)) {
+            return Path.of("lib/runtime");
+        }
         return Path.of("runtime");
     }
 
@@ -414,17 +419,18 @@
     private static boolean isFakeRuntime(Path runtimeDir, String msg) {
         final List<Path> criticalRuntimeFiles;
         if (Test.isWindows()) {
-            criticalRuntimeFiles = List.of(Path.of("server\\jvm.dll"));
+            criticalRuntimeFiles = List.of(Path.of("bin\\server\\jvm.dll"));
         } else if (Test.isLinux()) {
-            criticalRuntimeFiles = List.of(Path.of("server/libjvm.so"));
+            criticalRuntimeFiles = List.of(Path.of("lib/server/libjvm.so"));
         } else if (Test.isOSX()) {
-            criticalRuntimeFiles = List.of(Path.of("server/libjvm.dylib"));
+            criticalRuntimeFiles = List.of(Path.of("lib/server/libjvm.dylib"));
         } else {
             throw new IllegalArgumentException("Unknwon platform");
         }
 
-        if (criticalRuntimeFiles.stream().filter(v -> v.toFile().exists())
-                .findFirst().orElse(null) == null) {
+        if (criticalRuntimeFiles.stream().filter(
+                v -> runtimeDir.resolve(v).toFile().exists()).findFirst().orElse(
+                        null) == null) {
             // Fake runtime
             Test.trace(String.format(
                     "%s because application runtime directory [%s] is incomplete",
--- a/test/jdk/tools/jpackage/jdk/jpackage/internal/DeployParamsTest.java	Tue Sep 24 13:41:16 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,125 +0,0 @@
-/*
- * Copyright (c) 2018, 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.
- *
- * 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 jdk.jpackage.internal.Arguments;
-import jdk.jpackage.internal.DeployParams;
-import jdk.jpackage.internal.PackagerException;
-import java.io.File;
-
-/*
- * @test
- * @bug 8211285
- * @summary DeployParamsTest
- * @modules jdk.jpackage
- * @modules jdk.jpackage/jdk.jpackage.internal
- * @run main/othervm -Xmx512m DeployParamsTest
- */
-public class DeployParamsTest {
-
-    private static File testRoot = null;
-
-    private static void setUp() {
-        testRoot = new File("deployParamsTest");
-        System.out.println("DeployParamsTest: " + testRoot.getAbsolutePath());
-        testRoot.mkdir();
-    }
-
-    private static void tearDown() {
-        if (testRoot != null) {
-            testRoot.delete();
-        }
-    }
-
-    private static void testValidateAppName1() throws Exception {
-        DeployParams params = getParamsAppName();
-
-        setAppName(params, "Test");
-        params.validate();
-
-        setAppName(params, "Test Name");
-        params.validate();
-
-        setAppName(params, "Test - Name !!!");
-        params.validate();
-    }
-
-    private static void testValidateAppName2() throws Exception {
-        DeployParams params = getParamsAppName();
-
-        setAppName(params, "Test\nName");
-        appName2TestHelper(params);
-
-        setAppName(params, "Test\rName");
-        appName2TestHelper(params);
-
-        setAppName(params, "TestName\\");
-        appName2TestHelper(params);
-
-        setAppName(params, "Test \" Name");
-        appName2TestHelper(params);
-    }
-
-    private static void appName2TestHelper(DeployParams params) throws Exception {
-        try {
-            params.validate();
-        } catch (PackagerException pe) {
-            if (!pe.getMessage().startsWith("Error: Invalid Application name")) {
-                throw new Exception("Unexpected PackagerException received: " + pe);
-            }
-
-            return; // Done
-        }
-
-        throw new Exception("Expecting PackagerException");
-    }
-
-    // Returns deploy params initialized to pass all validation, except for
-    // app name
-    private static DeployParams getParamsAppName() {
-        DeployParams params = new DeployParams();
-
-        params.setOutput(testRoot);
-        params.addResource(testRoot, new File(testRoot, "test.jar"));
-        params.addBundleArgument(Arguments.CLIOptions.APPCLASS.getId(), "TestClass");
-        params.addBundleArgument(Arguments.CLIOptions.MAIN_JAR.getId(), "test.jar");
-        params.addBundleArgument(Arguments.CLIOptions.INPUT.getId(), "input");
-
-        return params;
-    }
-
-    private static void setAppName(DeployParams params, String appName) {
-        params.addBundleArgument(Arguments.CLIOptions.NAME.getId(), appName);
-    }
-
-    public static void main(String[] args) throws Exception {
-        setUp();
-
-        try {
-            testValidateAppName1();
-            testValidateAppName2();
-        } finally {
-            tearDown();
-        }
-    }
-
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jpackage/junit/jdk/jpackage/internal/ApplicationLayoutTest.java	Tue Sep 24 13:43:58 2019 -0400
@@ -0,0 +1,90 @@
+/*
+ * 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 org.junit.Test;
+import org.junit.Rule;
+import org.junit.rules.TemporaryFolder;
+import static org.junit.Assert.*;
+
+
+public class ApplicationLayoutTest {
+
+    @Rule
+    public final TemporaryFolder tempFolder = new TemporaryFolder();
+
+    private void fillLinuxAppImage() throws IOException {
+        appImage = tempFolder.newFolder("Foo").toPath();
+
+        Path base = appImage.getFileName();
+
+        tempFolder.newFolder(base.toString(), "bin");
+        tempFolder.newFolder(base.toString(), "lib", "app", "mods");
+        tempFolder.newFolder(base.toString(), "lib", "runtime", "bin");
+        tempFolder.newFile(base.resolve("bin/Foo").toString());
+        tempFolder.newFile(base.resolve("lib/app/Foo.cfg").toString());
+        tempFolder.newFile(base.resolve("lib/app/hello.jar").toString());
+        tempFolder.newFile(base.resolve("lib/Foo.png").toString());
+        tempFolder.newFile(base.resolve("lib/libapplauncher.so").toString());
+        tempFolder.newFile(base.resolve("lib/runtime/bin/java").toString());
+    }
+
+    @Test
+    public void testLinux() throws IOException {
+        fillLinuxAppImage();
+        testApplicationLayout(ApplicationLayout.linuxApp());
+    }
+
+    private void testApplicationLayout(ApplicationLayout layout) throws IOException {
+        ApplicationLayout srcLayout = layout.resolveAt(appImage);
+        assertApplicationLayout(srcLayout);
+
+        ApplicationLayout dstLayout = layout.resolveAt(
+                appImage.getParent().resolve(
+                        "Copy" + appImage.getFileName().toString()));
+        srcLayout.move(dstLayout);
+        Files.deleteIfExists(appImage);
+        assertApplicationLayout(dstLayout);
+
+        dstLayout.copy(srcLayout);
+        assertApplicationLayout(srcLayout);
+        assertApplicationLayout(dstLayout);
+    }
+
+    private void assertApplicationLayout(ApplicationLayout layout) throws IOException {
+        assertTrue(Files.isRegularFile(layout.appDirectory().resolve("Foo.cfg")));
+        assertTrue(Files.isRegularFile(layout.appDirectory().resolve("hello.jar")));
+        assertTrue(Files.isDirectory(layout.appModsDirectory()));
+        assertTrue(Files.isRegularFile(layout.launchersDirectory().resolve("Foo")));
+        assertTrue(Files.isRegularFile(layout.destktopIntegrationDirectory().resolve("Foo.png")));
+        assertTrue(Files.isRegularFile(layout.dllDirectory().resolve("libapplauncher.so")));
+        assertTrue(Files.isRegularFile(layout.runtimeDirectory().resolve("bin/java")));
+    }
+
+    private Path appImage;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jpackage/junit/jdk/jpackage/internal/DeployParamsTest.java	Tue Sep 24 13:43:58 2019 -0400
@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) 2018, 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.
+ *
+ * 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.File;
+import java.io.IOException;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.junit.Rule;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * Test for JDK-8211285
+ */
+public class DeployParamsTest {
+
+    @Rule
+    public final TemporaryFolder tempFolder = new TemporaryFolder();
+
+    @Rule
+    public final ExpectedException thrown = ExpectedException.none();
+
+    @Before
+    public void setUp() throws IOException {
+        testRoot = tempFolder.newFolder();
+    }
+
+    @Test
+    public void testValidAppName() throws PackagerException {
+        initParamsAppName();
+
+        setAppNameAndValidate("Test");
+
+        setAppNameAndValidate("Test Name");
+
+        setAppNameAndValidate("Test - Name !!!");
+    }
+
+    @Test
+    public void testInvalidAppName() throws PackagerException {
+        initForInvalidAppNamePackagerException();
+        initParamsAppName();
+        setAppNameAndValidate("Test\nName");
+    }
+
+    @Test
+    public void testInvalidAppName2() throws PackagerException {
+        initForInvalidAppNamePackagerException();
+        initParamsAppName();
+        setAppNameAndValidate("Test\rName");
+    }
+
+    @Test
+    public void testInvalidAppName3() throws PackagerException {
+        initForInvalidAppNamePackagerException();
+        initParamsAppName();
+        setAppNameAndValidate("TestName\\");
+    }
+
+    @Test
+    public void testInvalidAppName4() throws PackagerException {
+        initForInvalidAppNamePackagerException();
+        initParamsAppName();
+        setAppNameAndValidate("Test \" Name");
+    }
+
+    private void initForInvalidAppNamePackagerException() {
+        thrown.expect(PackagerException.class);
+
+        String msg = "Error: Invalid Application name";
+
+        // Unfortunately org.hamcrest.core.StringStartsWith is not available
+        // with older junit, DIY
+
+        // thrown.expectMessage(startsWith("Error: Invalid Application name"));
+        thrown.expectMessage(new BaseMatcher() {
+            @Override
+            @SuppressWarnings("unchecked")
+            public boolean matches(Object o) {
+                if (o instanceof String) {
+                    return ((String) o).startsWith(msg);
+                }
+                return false;
+            }
+
+            @Override
+            public void describeTo(Description d) {
+                d.appendText(msg);
+            }
+        });
+    }
+
+    // Returns deploy params initialized to pass all validation, except for
+    // app name
+    private void initParamsAppName() {
+        params = new DeployParams();
+
+        params.setOutput(testRoot);
+        params.addResource(testRoot, new File(testRoot, "test.jar"));
+        params.addBundleArgument(Arguments.CLIOptions.APPCLASS.getId(),
+                "TestClass");
+        params.addBundleArgument(Arguments.CLIOptions.MAIN_JAR.getId(),
+                "test.jar");
+        params.addBundleArgument(Arguments.CLIOptions.INPUT.getId(), "input");
+    }
+
+    private void setAppNameAndValidate(String appName) throws PackagerException {
+        params.addBundleArgument(Arguments.CLIOptions.NAME.getId(), appName);
+        params.validate();
+    }
+
+    private File testRoot = null;
+    private DeployParams params;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jpackage/junit/jdk/jpackage/internal/PathGroupTest.java	Tue Sep 24 13:43:58 2019 -0400
@@ -0,0 +1,131 @@
+/*
+ * 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 java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assert.*;
+import org.junit.Test;
+
+
+public class PathGroupTest {
+    public PathGroupTest() {
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testNullId() {
+         new PathGroup(Map.of()).getPath(null);
+    }
+
+    @Test
+    public void testEmptyPathGroup() {
+        PathGroup pg = new PathGroup(Map.of());
+
+        assertNull(pg.getPath("foo"));
+
+        assertEquals(0, pg.paths().size());
+        assertEquals(0, pg.roots().size());
+    }
+
+    @Test
+    public void testRootsSinglePath() {
+        final PathGroup pg = new PathGroup(Map.of("main", PATH_FOO));
+
+        List<Path> paths = pg.paths();
+        assertEquals(1, paths.size());
+        assertEquals(PATH_FOO, paths.iterator().next());
+
+        List<Path> roots = pg.roots();
+        assertEquals(1, roots.size());
+        assertEquals(PATH_FOO, roots.iterator().next());
+    }
+
+    @Test
+    public void testDuplicatedRoots() {
+        final PathGroup pg = new PathGroup(Map.of("main", PATH_FOO, "another",
+                PATH_FOO, "root", PATH_EMPTY));
+
+        List<Path> paths = pg.paths();
+        Collections.sort(paths);
+
+        assertEquals(3, paths.size());
+        assertEquals(PATH_EMPTY, paths.get(0));
+        assertEquals(PATH_FOO, paths.get(1));
+        assertEquals(PATH_FOO, paths.get(2));
+
+        List<Path> roots = pg.roots();
+        assertEquals(1, roots.size());
+        assertEquals(PATH_EMPTY, roots.get(0));
+    }
+
+    @Test
+    public void testRoots() {
+        final PathGroup pg = new PathGroup(Map.of(1, Path.of("foo"), 2, Path.of(
+                "foo", "bar"), 3, Path.of("foo", "bar", "buz")));
+
+        List<Path> paths = pg.paths();
+        assertEquals(3, paths.size());
+        assertTrue(paths.contains(Path.of("foo")));
+        assertTrue(paths.contains(Path.of("foo", "bar")));
+        assertTrue(paths.contains(Path.of("foo", "bar", "buz")));
+
+        List<Path> roots = pg.roots();
+        assertEquals(1, roots.size());
+        assertEquals(Path.of("foo"), roots.get(0));
+    }
+
+    @Test
+    public void testResolveAt() {
+        final PathGroup pg = new PathGroup(Map.of(0, PATH_FOO, 1, PATH_BAR, 2,
+                PATH_EMPTY));
+
+        final Path aPath = Path.of("a");
+
+        final PathGroup pg2 = pg.resolveAt(aPath);
+        assertThat(pg, not(equalTo(pg2)));
+
+        List<Path> paths = pg.paths();
+        assertEquals(3, paths.size());
+        assertTrue(paths.contains(PATH_EMPTY));
+        assertTrue(paths.contains(PATH_FOO));
+        assertTrue(paths.contains(PATH_BAR));
+        assertEquals(PATH_EMPTY, pg.roots().get(0));
+
+        paths = pg2.paths();
+        assertEquals(3, paths.size());
+        assertTrue(paths.contains(aPath.resolve(PATH_EMPTY)));
+        assertTrue(paths.contains(aPath.resolve(PATH_FOO)));
+        assertTrue(paths.contains(aPath.resolve(PATH_BAR)));
+        assertEquals(aPath, pg2.roots().get(0));
+    }
+
+    private final static Path PATH_FOO = Path.of("foo");
+    private final static Path PATH_BAR = Path.of("bar");
+    private final static Path PATH_EMPTY = Path.of("");
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jpackage/junit/junit.java	Tue Sep 24 13:43:58 2019 -0400
@@ -0,0 +1,9 @@
+/*
+ * @test
+ * @summary jpackage unit tests
+ * @library ${jtreg.home}/lib/junit.jar
+ * @run shell run_junit.sh
+ *  jdk.jpackage.internal.PathGroupTest
+ *  jdk.jpackage.internal.DeployParamsTest
+ *  jdk.jpackage.internal.ApplicationLayoutTest
+ */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jpackage/junit/run_junit.sh	Tue Sep 24 13:43:58 2019 -0400
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+set -x
+
+set -e
+if [ -z "$BASH" ]; then
+  # The script relies on Bash arrays, rerun in Bash.
+  /bin/bash $0 $@
+  exit
+fi
+
+classes=( "$@" )
+sources=()
+for c in "${classes[@]}"; do
+  sources+=( "${TESTSRC}/$(echo $c | sed -e 's|\.|/|g').java" )
+done
+
+common_args=(\
+  --patch-module jdk.jpackage="${TESTSRC}${PS}${TESTCLASSES}" \
+  --add-reads jdk.jpackage=ALL-UNNAMED \
+  --add-exports jdk.jpackage/jdk.jpackage.internal=ALL-UNNAMED \
+  -classpath "${TESTCLASSPATH}" \
+)
+
+# Compile classes for junit
+"${COMPILEJAVA}/bin/javac" ${TESTTOOLVMOPTS} ${TESTJAVACOPTS} \
+  "${common_args[@]}" -d "${TESTCLASSES}" "${sources[@]}"
+
+# Run junit
+"${TESTJAVA}/bin/java" ${TESTVMOPTS} ${TESTJAVAOPTS} \
+  "${common_args[@]}" org.junit.runner.JUnitCore "$@"
--- a/test/jdk/tools/jpackage/linux/BundleNameTest.java	Tue Sep 24 13:41:16 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,70 +0,0 @@
-/*
- * 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.
- *
- * 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 jdk.jpackage.test.Test;
-import jdk.jpackage.test.PackageTest;
-import jdk.jpackage.test.PackageType;
-
-
-/**
- * Test --linux-package-name parameter. Output of the test should be
- * quickbrownfox2_1.0-1_amd64.deb or quickbrownfox2-1.0-1.amd64.rpm package
- * bundle. The output package should provide the same functionality as the
- * default package.
- *
- * deb:
- * Package property of the package should be set to quickbrownfox2.
- *
- * rpm:
- * Name property of the package should be set to quickbrownfox2.
- */
-
-
-/*
- * @test
- * @summary jpackage with --linux-package-name
- * @library ../helpers
- * @requires (os.family == "linux")
- * @modules jdk.jpackage/jdk.jpackage.internal
- * @run main/othervm/timeout=360 -Xmx512m BundleNameTest
- */
-public class BundleNameTest {
-
-    public static void main(String[] args) {
-        final String PACKAGE_NAME = "quickbrownfox2";
-
-        Test.run(args, () -> {
-            new PackageTest()
-            .forTypes(PackageType.LINUX)
-            .configureHelloApp()
-            .addInitializer(cmd -> {
-                cmd.addArguments("--linux-package-name", PACKAGE_NAME);
-            })
-            .forTypes(PackageType.LINUX_DEB)
-            .addBundlePropertyVerifier("Package", PACKAGE_NAME)
-            .forTypes(PackageType.LINUX_RPM)
-            .addBundlePropertyVerifier("Name", PACKAGE_NAME)
-            .run();
-        });
-    }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jpackage/linux/LinuxBundleNameTest.java	Tue Sep 24 13:43:58 2019 -0400
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ *
+ * 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 jdk.jpackage.test.Test;
+import jdk.jpackage.test.PackageTest;
+import jdk.jpackage.test.PackageType;
+
+
+/**
+ * Test --linux-package-name parameter. Output of the test should be
+ * quickbrownfox2_1.0-1_amd64.deb or quickbrownfox2-1.0-1.amd64.rpm package
+ * bundle. The output package should provide the same functionality as the
+ * default package.
+ *
+ * deb:
+ * Package property of the package should be set to quickbrownfox2.
+ *
+ * rpm:
+ * Name property of the package should be set to quickbrownfox2.
+ */
+
+
+/*
+ * @test
+ * @summary jpackage with --linux-package-name
+ * @library ../helpers
+ * @requires (os.family == "linux")
+ * @modules jdk.jpackage/jdk.jpackage.internal
+ * @run main/othervm/timeout=360 -Xmx512m LinuxBundleNameTest
+ */
+public class LinuxBundleNameTest {
+
+    public static void main(String[] args) {
+        final String PACKAGE_NAME = "quickbrownfox2";
+
+        Test.run(args, () -> {
+            new PackageTest()
+            .forTypes(PackageType.LINUX)
+            .configureHelloApp()
+            .addInitializer(cmd -> {
+                cmd.addArguments("--linux-package-name", PACKAGE_NAME);
+            })
+            .forTypes(PackageType.LINUX_DEB)
+            .addBundlePropertyVerifier("Package", PACKAGE_NAME)
+            .forTypes(PackageType.LINUX_RPM)
+            .addBundlePropertyVerifier("Name", PACKAGE_NAME)
+            .run();
+        });
+    }
+}