diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationLayout.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationLayout.java index c89131b2d4b..230e4beb49b 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationLayout.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ApplicationLayout.java @@ -26,14 +26,16 @@ package jdk.jpackage.internal; import jdk.internal.util.OperatingSystem; import jdk.jpackage.internal.util.PathGroup; +import java.io.IOException; import java.nio.file.Path; +import java.util.List; import java.util.Map; /** * Application directory layout. */ -public final class ApplicationLayout implements PathGroup.Facade { +public final class ApplicationLayout { enum PathRole { /** * Java run-time directory. @@ -84,16 +86,34 @@ public final class ApplicationLayout implements PathGroup.Facade roots() { + return data.roots(); + } + + public long sizeInBytes() throws IOException { + return data.sizeInBytes(); + } + + public void copy(ApplicationLayout other) throws IOException { + data.copy(other.data); + } + + public void move(ApplicationLayout other) throws IOException { + data.move(other.data); + } + + public void transform(ApplicationLayout other, PathGroup.TransformHandler handler) throws IOException { + data.transform(other.data, handler); + } + /** * Path to launchers directory. */ diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/PathGroup.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/PathGroup.java index c6938b6b986..bad4bc3069f 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/PathGroup.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/PathGroup.java @@ -24,36 +24,69 @@ */ package jdk.jpackage.internal.util; +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toMap; +import static java.util.stream.Collectors.toSet; + import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.CopyOption; +import java.nio.file.FileVisitOption; import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; +import java.util.Comparator; 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.Objects; +import java.util.Set; import java.util.stream.Stream; - /** - * Group of paths. - * Each path in the group is assigned a unique id. + * Group of paths. Each path in the group is assigned a unique id. */ public final class PathGroup { + + /** + * Creates path group with the initial paths. + * + * @param paths the initial paths + */ public PathGroup(Map paths) { + paths.keySet().forEach(Objects::requireNonNull); + paths.values().forEach(Objects::requireNonNull); entries = new HashMap<>(paths); } + /** + * Returns a path associated with the given identifier in this path group. + * + * @param id the identifier + * @return the path corresponding to the given identifier in this path group or + * null if there is no such path + */ public Path getPath(Object id) { - if (id == null) { - throw new NullPointerException(); - } + Objects.requireNonNull(id); return entries.get(id); } + /** + * Assigns the specified path value to the given identifier in this path group. + * If the given identifier doesn't exist in this path group, it is added, + * otherwise, the current value associated with the identifier is replaced with + * the given path value. If the path value is null the given + * identifier is removed from this path group if it existed; otherwise, no + * action is taken. + * + * @param id the identifier + * @param path the path to associate with the identifier or null + */ public void setPath(Object id, Path path) { + Objects.requireNonNull(id); if (path != null) { entries.put(id, path); } else { @@ -62,196 +95,379 @@ public final class PathGroup { } /** - * All configured entries. + * Adds a path associated with the new unique identifier to this path group. + * + * @param path the path to associate the new unique identifier in this path + * group + */ + public void ghostPath(Path path) { + Objects.requireNonNull(path); + setPath(new Object(), path); + } + + /** + * Gets all identifiers of this path group. + *

+ * The order of identifiers in the returned list is undefined. + * + * @return all identifiers of this path group + */ + public Set keys() { + return entries.keySet(); + } + + /** + * Gets paths associated with all identifiers in this path group. + *

+ * The order of paths in the returned list is undefined. + * + * @return paths associated with all identifiers in this path group */ public List paths() { return entries.values().stream().toList(); } /** - * Root entries. + * Gets root paths in this path group. + *

+ * If multiple identifiers are associated with the same path value in the group, + * the path value is added to the returned list only once. Paths that are + * descendants of other paths in the group are not added to the returned list. + *

+ * The order of paths in the returned list is undefined. + * + * @return unique root paths in this path group */ public List roots() { - // Sort by the number of path components in ascending order. - List> sorted = normalizedPaths().stream().sorted( - (a, b) -> a.getKey().getNameCount() - b.getKey().getNameCount()).toList(); + if (entries.isEmpty()) { + return List.of(); + } - // Returns `true` if `a` is a parent of `b` - BiFunction, Map.Entry, Boolean> isParentOrSelf = (a, b) -> { - return a == b || b.getKey().startsWith(a.getKey()); - }; + // Sort by the number of path components in descending order. + final var sorted = entries.entrySet().stream().map(e -> { + return Map.entry(e.getValue().normalize(), e.getValue()); + }).sorted(Comparator.comparingInt(e -> e.getValue().getNameCount() * -1)).distinct().toList(); - return sorted.stream().filter( - v -> v == sorted.stream().sequential().filter( - v2 -> isParentOrSelf.apply(v2, v)).findFirst().get()).map( - v -> v.getValue()).toList(); + final var shortestNormalizedPath = sorted.getLast().getKey(); + if (shortestNormalizedPath.getNameCount() == 1 && shortestNormalizedPath.getFileName().toString().isEmpty()) { + return List.of(sorted.getLast().getValue()); + } + + final List roots = new ArrayList<>(); + + for (int i = 0; i < sorted.size(); ++i) { + final var path = sorted.get(i).getKey(); + boolean pathIsRoot = true; + for (int j = i + 1; j < sorted.size(); ++j) { + final var maybeParent = sorted.get(j).getKey(); + if (path.getNameCount() > maybeParent.getNameCount() && path.startsWith(maybeParent)) { + pathIsRoot = false; + break; + } + } + + if (pathIsRoot) { + roots.add(sorted.get(i).getValue()); + } + } + + return roots; } + /** + * Gets the number of bytes in root paths of this path group. The method sums + * the size of all root path entries in the group. If the path entry is a + * directory it calculates the total size of the files in the directory. If the + * path entry is a file, it takes its size. + * + * @return the total number of bytes in root paths of this path group + * @throws IOException If an I/O error occurs + */ public long sizeInBytes() throws IOException { long reply = 0; - for (Path dir : roots().stream().filter(f -> Files.isDirectory(f)).collect( - Collectors.toList())) { - try (Stream stream = Files.walk(dir)) { - reply += stream.filter(p -> Files.isRegularFile(p)).mapToLong( - f -> f.toFile().length()).sum(); + final var roots = roots(); + try { + for (Path dir : roots.stream().filter(Files::isDirectory).toList()) { + try (Stream stream = Files.walk(dir)) { + reply += stream.mapToLong(PathGroup::sizeInBytes).sum(); + } } + reply += roots.stream().mapToLong(PathGroup::sizeInBytes).sum(); + } catch (UncheckedIOException ex) { + throw ex.getCause(); } return reply; } + private static long sizeInBytes(Path path) throws UncheckedIOException { + if (Files.isRegularFile(path)) { + try { + return Files.size(path); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } else { + return 0; + } + } + + /** + * Creates a copy of this path group with all paths resolved against the given + * root. Taken action is equivalent to creating a copy of this path group and + * calling root.resolve() on every path in the copy. + * + * @param root the root against which to resolve paths + * + * @return a new path group resolved against the given root path + */ public PathGroup resolveAt(Path root) { - return new PathGroup(entries.entrySet().stream().collect( - Collectors.toMap(e -> e.getKey(), - e -> root.resolve(e.getValue())))); + Objects.requireNonNull(root); + return new PathGroup(entries.entrySet().stream() + .collect(toMap(Map.Entry::getKey, e -> root.resolve(e.getValue())))); } - public void copy(PathGroup dst) throws IOException { - copy(this, dst, null, false); + /** + * Copies files/directories from the locations in the path group into the + * locations of the given path group. For every identifier found in this and the + * given group, copy the associated file or directory from the location + * specified by the path value associated with the identifier in this group into + * the location associated with the identifier in the given group. + * + * @param dst the destination path group + * @throws IOException If an I/O error occurs + */ + public void copy(PathGroup dst, CopyOption ...options) throws IOException { + final var handler = new Copy(false, options); + copy(this, dst, handler, handler.followSymlinks()); } - public void move(PathGroup dst) throws IOException { - copy(this, dst, null, true); + /** + * Similar to {@link #copy(PathGroup)} but moves files/directories instead of + * copying. + * + * @param dst the destination path group + * @throws IOException If an I/O error occurs + */ + public void move(PathGroup dst, CopyOption ...options) throws IOException { + final var handler = new Copy(true, options); + copy(this, dst, handler, handler.followSymlinks()); + deleteEntries(); } + /** + * Similar to {@link #copy(PathGroup)} but uses the given handler to transform + * paths instead of coping. + * + * @param dst the destination path group + * @param handler the path transformation handler + * @throws IOException If an I/O error occurs + */ public void transform(PathGroup dst, TransformHandler handler) throws IOException { copy(this, dst, handler, false); } - public static interface Facade { - PathGroup pathGroup(); - - default Collection paths() { - return pathGroup().paths(); - } - - default List roots() { - return pathGroup().roots(); - } - - default long sizeInBytes() throws IOException { - return pathGroup().sizeInBytes(); - } - - T resolveAt(Path root); - - default void copy(Facade dst) throws IOException { - pathGroup().copy(dst.pathGroup()); - } - - default void move(Facade dst) throws IOException { - pathGroup().move(dst.pathGroup()); - } - - default void transform(Facade dst, TransformHandler handler) throws - IOException { - pathGroup().transform(dst.pathGroup(), handler); - } - } - + /** + * Handler of file copying and directory creating. + * + * @see #transform + */ public static interface TransformHandler { - void copyFile(Path src, Path dst) throws IOException; - void createDirectory(Path dir) throws IOException; + + /** + * Request to copy a file from the given source location into the given + * destination location. + * + * @implNote Default implementation takes no action + * + * @param src the source file location + * @param dst the destination file location + * @throws IOException If an I/O error occurs + */ + default void copyFile(Path src, Path dst) throws IOException { + + } + + /** + * Request to create a directory at the given location. + * + * @implNote Default implementation takes no action + * + * @param dir the path where the directory is requested to be created + * @throws IOException + */ + default void createDirectory(Path dir) throws IOException { + + } + + /** + * Request to copy a symbolic link from the given source location into the given + * destination location. + * + * @implNote Default implementation calls {@link #copyFile}. + * + * @param src the source symbolic link location + * @param dst the destination symbolic link location + * @throws IOException If an I/O error occurs + */ + default void copySymbolicLink(Path src, Path dst) throws IOException { + copyFile(src, dst); + } } - private static void copy(PathGroup src, PathGroup dst, - TransformHandler handler, boolean move) throws IOException { - List> copyItems = new ArrayList<>(); - List excludeItems = new ArrayList<>(); - - for (var id: src.entries.keySet()) { - Path srcPath = src.entries.get(id); - if (dst.entries.containsKey(id)) { - copyItems.add(Map.entry(srcPath, dst.entries.get(id))); + private void deleteEntries() throws IOException { + for (final var file : entries.values()) { + if (Files.isDirectory(file)) { + FileUtils.deleteRecursive(file); } else { - excludeItems.add(srcPath); + Files.deleteIfExists(file); + } + } + } + + private record CopySpec(Path basepath, Path from, Path to) { + CopySpec { + Objects.requireNonNull(basepath); + Objects.requireNonNull(to); + if (!from.startsWith(basepath)) { + throw new IllegalArgumentException(); } } - copy(move, copyItems, excludeItems, handler); + @Override + public int hashCode() { + return Objects.hash(from, to); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + CopySpec other = (CopySpec) obj; + return Objects.equals(from, other.from) && Objects.equals(to, other.to); + } + + Path fromNormalized() { + return from().normalize(); + } + + Path toNormalized() { + return to().normalize(); + } + + CopySpec(Path from, Path to) { + this(from, from, to); + } } - private static void copy(boolean move, List> entries, - List excludePaths, TransformHandler handler) throws - IOException { + private static void copy(PathGroup src, PathGroup dst, TransformHandler handler, boolean followSymlinks) throws IOException { + List copySpecs = new ArrayList<>(); + List excludePaths = new ArrayList<>(); - if (handler == null) { - handler = new TransformHandler() { - @Override - public void copyFile(Path src, Path dst) throws IOException { - Files.createDirectories(dst.getParent()); - if (move) { - Files.move(src, dst); - } else { - Files.copy(src, dst); + for (final var e : src.entries.entrySet()) { + final var srcPath = e.getValue(); + final var dstPath = dst.entries.get(e.getKey()); + if (dstPath != null) { + copySpecs.add(new CopySpec(srcPath, dstPath)); + } else { + excludePaths.add(srcPath.normalize()); + } + } + + copy(copySpecs, excludePaths, handler, followSymlinks); + } + + private record Copy(boolean move, boolean followSymlinks, CopyOption ... options) implements TransformHandler { + + Copy(boolean move, CopyOption ... options) { + this(move, !Set.of(options).contains(LinkOption.NOFOLLOW_LINKS), options); + } + + @Override + public void copyFile(Path src, Path dst) throws IOException { + Files.createDirectories(dst.getParent()); + if (move) { + Files.move(src, dst, options); + } else { + Files.copy(src, dst, options); + } + } + + @Override + public void createDirectory(Path dir) throws IOException { + Files.createDirectories(dir); + } + } + + private static boolean match(Path what, List paths) { + return paths.stream().anyMatch(what::startsWith); + } + + private static void copy(List copySpecs, List excludePaths, + TransformHandler handler, boolean followSymlinks) throws IOException { + Objects.requireNonNull(excludePaths); + Objects.requireNonNull(handler); + + + final var filteredCopySpecs = copySpecs.stream().mapMulti((copySpec, consumer) -> { + final var src = copySpec.from(); + + if (!Files.exists(src) || match(src, excludePaths)) { + return; + } + + if (Files.isDirectory(copySpec.from())) { + final var dst = copySpec.to; + final var walkMode = followSymlinks ? new FileVisitOption[] { FileVisitOption.FOLLOW_LINKS } : new FileVisitOption[0]; + try (final var files = Files.walk(src, walkMode)) { + files.filter(file -> { + return !match(file, excludePaths); + }).map(file -> { + return new CopySpec(src, file, dst.resolve(src.relativize(file))); + }).toList().forEach(consumer::accept); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } else { + consumer.accept(copySpec); + } + }).collect(groupingBy(CopySpec::fromNormalized, collectingAndThen(toSet(), copySpecGroup -> { + return copySpecGroup.stream().filter(copySpec -> { + for (final var otherCopySpec : copySpecGroup) { + if (otherCopySpec != copySpec && !otherCopySpec.basepath().equals(copySpec.basepath()) + && otherCopySpec.basepath().startsWith(copySpec.basepath())) { + return false; } } + return true; + }).toList(); + }))).values().stream().flatMap(Collection::stream).toList(); - @Override - public void createDirectory(Path dir) throws IOException { - Files.createDirectories(dir); + filteredCopySpecs.stream().collect(toMap(CopySpec::toNormalized, x -> x, (x, y) -> { + throw new IllegalStateException(String.format( + "Duplicate source files [%s] and [%s] for [%s] destination file", x.from(), y.from(), x.to())); + })); + + try { + filteredCopySpecs.stream().forEach(copySpec -> { + try { + if (Files.isSymbolicLink(copySpec.from())) { + handler.copySymbolicLink(copySpec.from(), copySpec.to()); + } else if (Files.isDirectory(copySpec.from())) { + handler.createDirectory(copySpec.to()); + } else { + handler.copyFile(copySpec.from(), copySpec.to()); + } + } catch (IOException ex) { + throw new UncheckedIOException(ex); } - }; + }); + } catch (UncheckedIOException ex) { + throw ex.getCause(); } - - // destination -> source file mapping - Map actions = new HashMap<>(); - for (var action: entries) { - Path src = action.getKey(); - Path dst = action.getValue(); - if (Files.isDirectory(src)) { - try (Stream stream = Files.walk(src)) { - stream.sequential().forEach(path -> actions.put(dst.resolve( - src.relativize(path)).normalize(), path)); - } - } else { - actions.put(dst.normalize(), src); - } - } - - for (var action : actions.entrySet()) { - Path dst = action.getKey(); - Path src = action.getValue(); - - if (excludePaths.stream().anyMatch(src::startsWith)) { - continue; - } - - if (src.equals(dst) || !src.toFile().exists()) { - continue; - } - - if (Files.isDirectory(src)) { - handler.createDirectory(dst); - } else { - handler.copyFile(src, dst); - } - } - - if (move) { - // Delete source dirs. - for (var entry: entries) { - Path srcFile = entry.getKey(); - if (Files.isDirectory(srcFile)) { - FileUtils.deleteRecursive(srcFile); - } - } - } - } - - private static Map.Entry normalizedPath(Path v) { - final Path normalized; - if (!v.isAbsolute()) { - normalized = Path.of("./").resolve(v.normalize()); - } else { - normalized = v.normalize(); - } - - return Map.entry(normalized, v); - } - - private List> normalizedPaths() { - return entries.values().stream().map(PathGroup::normalizedPath).collect( - Collectors.toList()); } private final Map entries; diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ApplicationLayout.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ApplicationLayout.java index 88ce7169da7..7ab3b824aa4 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ApplicationLayout.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ApplicationLayout.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2025, 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 @@ -29,7 +29,7 @@ import java.util.Optional; public record ApplicationLayout(Path launchersDirectory, Path appDirectory, Path runtimeDirectory, Path runtimeHomeDirectory, Path appModsDirectory, - Path destktopIntegrationDirectory, Path contentDirectory) { + Path desktopIntegrationDirectory, Path contentDirectory, Path libapplauncher) { public ApplicationLayout resolveAt(Path root) { return new ApplicationLayout( @@ -38,8 +38,9 @@ public record ApplicationLayout(Path launchersDirectory, Path appDirectory, resolve(root, runtimeDirectory), resolve(root, runtimeHomeDirectory), resolve(root, appModsDirectory), - resolve(root, destktopIntegrationDirectory), - resolve(root, contentDirectory)); + resolve(root, desktopIntegrationDirectory), + resolve(root, contentDirectory), + resolve(root, libapplauncher)); } public static ApplicationLayout linuxAppImage() { @@ -50,7 +51,8 @@ public record ApplicationLayout(Path launchersDirectory, Path appDirectory, Path.of("lib/runtime"), Path.of("lib/app/mods"), Path.of("lib"), - Path.of("lib") + Path.of("lib"), + Path.of("lib/libapplauncher.so") ); } @@ -62,7 +64,8 @@ public record ApplicationLayout(Path launchersDirectory, Path appDirectory, Path.of("runtime"), Path.of("app/mods"), Path.of(""), - Path.of("") + Path.of(""), + null ); } @@ -74,7 +77,8 @@ public record ApplicationLayout(Path launchersDirectory, Path appDirectory, Path.of("Contents/runtime/Contents/Home"), Path.of("Contents/app/mods"), Path.of("Contents/Resources"), - Path.of("Contents") + Path.of("Contents"), + null ); } @@ -102,6 +106,7 @@ public record ApplicationLayout(Path launchersDirectory, Path appDirectory, null, null, null, + null, null ); } @@ -116,7 +121,8 @@ public record ApplicationLayout(Path launchersDirectory, Path appDirectory, lib.resolve("runtime"), lib.resolve("app/mods"), lib, - lib + lib, + lib.resolve("lib/libapplauncher.so") ); } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java index 722515887b8..8c5a653f5a0 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java @@ -1008,9 +1008,9 @@ public class JPackageCommand extends CommandArguments { final Path lookupPath = AppImageFile.getPathInAppImage(appImageDir); if (!expectAppImageFile()) { - assertFileInAppImage(lookupPath, null); + assertFileNotInAppImage(lookupPath); } else { - assertFileInAppImage(lookupPath, lookupPath); + assertFileInAppImage(lookupPath); if (TKit.isOSX()) { final Path rootDir = isImagePackageType() ? outputBundle() : @@ -1035,22 +1035,39 @@ public class JPackageCommand extends CommandArguments { final Path lookupPath = PackageFile.getPathInAppImage(Path.of("")); if (isRuntime() || isImagePackageType() || TKit.isLinux()) { - assertFileInAppImage(lookupPath, null); + assertFileNotInAppImage(lookupPath); } else { if (TKit.isOSX() && hasArgument("--app-image")) { String appImage = getArgumentValue("--app-image"); if (AppImageFile.load(Path.of(appImage)).macSigned()) { - assertFileInAppImage(lookupPath, null); + assertFileNotInAppImage(lookupPath); } else { - assertFileInAppImage(lookupPath, lookupPath); + assertFileInAppImage(lookupPath); } } else { - assertFileInAppImage(lookupPath, lookupPath); + assertFileInAppImage(lookupPath); } } } + public void assertFileInAppImage(Path expectedPath) { + assertFileInAppImage(expectedPath.getFileName(), expectedPath); + } + + public void assertFileNotInAppImage(Path filename) { + assertFileInAppImage(filename, null); + } + private void assertFileInAppImage(Path filename, Path expectedPath) { + if (expectedPath != null) { + if (expectedPath.isAbsolute()) { + throw new IllegalArgumentException(); + } + if (!expectedPath.getFileName().equals(filename.getFileName())) { + throw new IllegalArgumentException(); + } + } + if (filename.getNameCount() > 1) { assertFileInAppImage(filename.getFileName(), expectedPath); return; diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherIconVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherIconVerifier.java index a5eb24980ae..a1971ee0835 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherIconVerifier.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherIconVerifier.java @@ -57,7 +57,7 @@ public final class LauncherIconVerifier { label = String.format("[%s]", launcherName); } - Path iconPath = cmd.appLayout().destktopIntegrationDirectory().resolve( + Path iconPath = cmd.appLayout().desktopIntegrationDirectory().resolve( curLauncherName + TKit.ICON_SUFFIX); if (TKit.isWindows()) { diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java index b0291fc52eb..b5786a5065a 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java @@ -82,7 +82,7 @@ public final class LinuxHelper { String desktopFileName = String.format("%s-%s.desktop", getPackageName( cmd), Optional.ofNullable(launcherName).orElseGet( () -> cmd.name()).replaceAll("\\s+", "_")); - return cmd.appLayout().destktopIntegrationDirectory().resolve( + return cmd.appLayout().desktopIntegrationDirectory().resolve( desktopFileName); } @@ -393,7 +393,7 @@ public final class LinuxHelper { test.addInstallVerifier(cmd -> { // Verify .desktop files. - try (var files = Files.list(cmd.appLayout().destktopIntegrationDirectory())) { + try (var files = Files.list(cmd.appLayout().desktopIntegrationDirectory())) { List desktopFiles = files .filter(path -> path.getFileName().toString().endsWith(".desktop")) .toList(); @@ -470,7 +470,7 @@ public final class LinuxHelper { for (var e : List.>, Function>>of( Map.entry(Map.entry("Exec", Optional.of(launcherPath)), ApplicationLayout::launchersDirectory), - Map.entry(Map.entry("Icon", Optional.empty()), ApplicationLayout::destktopIntegrationDirectory))) { + Map.entry(Map.entry("Icon", Optional.empty()), ApplicationLayout::desktopIntegrationDirectory))) { var path = e.getKey().getValue().or(() -> Optional.of(data.get( e.getKey().getKey()))).map(Path::of).get(); TKit.assertFileExists(cmd.pathToUnpackedPackageFile(path)); @@ -537,7 +537,7 @@ public final class LinuxHelper { Path systemDesktopFile = getSystemDesktopFilesFolder().resolve( desktopFileName); - Path appDesktopFile = cmd.appLayout().destktopIntegrationDirectory().resolve( + Path appDesktopFile = cmd.appLayout().desktopIntegrationDirectory().resolve( desktopFileName); TKit.assertFileExists(systemDesktopFile); @@ -569,7 +569,7 @@ public final class LinuxHelper { final Path mimeTypeIconFileName = fa.getLinuxIconFileName(); if (mimeTypeIconFileName != null) { // Verify there are xdg registration commands for mime icon file. - Path mimeTypeIcon = cmd.appLayout().destktopIntegrationDirectory().resolve( + Path mimeTypeIcon = cmd.appLayout().desktopIntegrationDirectory().resolve( mimeTypeIconFileName); Map> scriptlets = getScriptlets(cmd); diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/PathGroupTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/PathGroupTest.java index 42578227cf5..c46dbb148de 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/PathGroupTest.java +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/PathGroupTest.java @@ -23,16 +23,9 @@ package jdk.jpackage.internal.util; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.function.Consumer; -import java.util.function.UnaryOperator; -import java.util.stream.Stream; +import static java.util.stream.Collectors.toMap; +import static java.util.stream.Collectors.toSet; +import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -40,6 +33,25 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrowsExactly; import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.io.UncheckedIOException; +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.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.UnaryOperator; +import java.util.stream.IntStream; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; @@ -113,6 +125,69 @@ public class PathGroupTest { assertEquals(Path.of("foo"), roots.get(0)); } + public record TestRootsSpec(Collection paths, Set expectedRoots) { + public TestRootsSpec { + paths.forEach(Objects::requireNonNull); + expectedRoots.forEach(Objects::requireNonNull); + } + + void test() { + final var pg = new PathGroup(paths.stream().collect(toMap(x -> new Object(), x -> x))); + final var actualRoots = pg.roots().stream().collect(toSet()); + assertEquals(expectedRoots, actualRoots); + } + + static TestRootsSpec create(Collection paths, Set expectedRoots) { + return new TestRootsSpec(paths.stream().map(Path::of).toList(), expectedRoots.stream().map(Path::of).collect(toSet())); + } + } + + @ParameterizedTest + @MethodSource("testRootsValues") + public void testRoots(TestRootsSpec testSpec) { + testSpec.test(); + } + + private static Collection testRootsValues() { + return List.of( + TestRootsSpec.create(List.of(""), Set.of("")), + TestRootsSpec.create(List.of("/", "a/b/c", "a/b", "a/b", "a/b/"), Set.of("a/b", "/")) + ); + } + + @ParameterizedTest + @MethodSource + public void testSizeInBytes(List paths, @TempDir Path tempDir) throws IOException { + final var files = Set.of("AA.txt", "a/b/c/BB.txt", "a/b/c/DD.txt", "d/foo.txt").stream().map(Path::of).toList(); + + int counter = 0; + for (var file : files) { + file = tempDir.resolve(file); + Files.createDirectories(file.getParent()); + Files.writeString(file, "x".repeat(++counter * 100)); + } + + final var expectedSize = Stream.of(walkFiles(tempDir)) + .map(tempDir::resolve) + .filter(Files::isRegularFile) + .map(toFunction(Files::size)) + .mapToLong(Long::longValue).sum(); + + final var pg = new PathGroup(paths.stream().collect(toMap(x -> new Object(), x -> x))).resolveAt(tempDir); + + assertEquals(expectedSize, pg.sizeInBytes()); + } + + private static Collection> testSizeInBytes() { + return Stream.of( + List.of(""), + List.of("AA.txt", "a/b", "d", "non-existant", "non-existant/foo/bar"), + List.of("AA.txt", "a/b", "d", "a/b/c/BB.txt", "a/b/c/BB.txt") + ).map(v -> { + return v.stream().map(Path::of).toList(); + }).toList(); + } + @Test public void testResolveAt() { final PathGroup pg = new PathGroup(Map.of( @@ -140,7 +215,7 @@ public class PathGroupTest { assertEquals(aPath, pg2.roots().get(0)); } - enum TransformType { COPY, MOVE, HANDLER }; + enum TransformType { COPY, MOVE, HANDLER } private static Stream testTransform() { return Stream.of(TransformType.values()).flatMap(transform -> { @@ -265,6 +340,253 @@ public class PathGroupTest { } } + private enum PathRole { + DESKTOP, + LINUX_APPLAUNCHER_LIB, + } + + private static PathGroup linuxAppImage() { + return new PathGroup(Map.of( + PathRole.DESKTOP, Path.of("lib"), + PathRole.LINUX_APPLAUNCHER_LIB, Path.of("lib/libapplauncher.so") + )); + } + + private static PathGroup linuxUsrTreePackageImage(Path prefix, String packageName) { + final Path lib = prefix.resolve(Path.of("lib", packageName)); + return new PathGroup(Map.of( + PathRole.DESKTOP, lib, + PathRole.LINUX_APPLAUNCHER_LIB, lib.resolve("lib/libapplauncher.so") + )); + } + + @Test + public void testLinuxLibapplauncher(@TempDir Path tempDir) throws IOException { + final Path srcDir = tempDir.resolve("src"); + final Path dstDir = tempDir.resolve("dst"); + + final Map> files = Map.of( + PathRole.LINUX_APPLAUNCHER_LIB, List.of(Path.of("")), + PathRole.DESKTOP, List.of(Path.of("UsrUsrTreeTest.png")) + ); + + final var srcAppLayout = linuxAppImage().resolveAt(srcDir); + final var dstAppLayout = linuxUsrTreePackageImage(dstDir.resolve("usr"), "foo"); + + for (final var e : files.entrySet()) { + final var pathRole = e.getKey(); + final var basedir = srcAppLayout.getPath(pathRole); + for (var file : e.getValue().stream().map(basedir::resolve).toList()) { + Files.createDirectories(file.getParent()); + Files.writeString(file, "foo"); + } + } + + srcAppLayout.copy(dstAppLayout); + + Stream.of(walkFiles(dstDir)).filter(p -> { + return p.getFileName().equals(dstAppLayout.getPath(PathRole.LINUX_APPLAUNCHER_LIB).getFileName()); + }).reduce((x, y) -> { + throw new AssertionError(String.format("Multiple libapplauncher: [%s], [%s]", x, y)); + }); + } + + + public enum TestFileContent { + A, + B, + C; + + void assertFileContent(Path path) { + try { + final var expected = name(); + final var actual = Files.readString(path); + assertEquals(expected, actual); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + void createFile(Path path) { + try { + Files.createDirectories(path.getParent()); + Files.writeString(path, name()); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + } + + public record PathGroupCopy(Path from, Path to) { + public PathGroupCopy { + Objects.requireNonNull(from); + } + + public PathGroupCopy(Path from) { + this(from, null); + } + } + + public record TestFile(Path path, TestFileContent content) { + public TestFile { + Objects.requireNonNull(content); + if (path.isAbsolute()) { + throw new IllegalArgumentException(); + } + } + + void assertFileContent(Path basedir) { + content.assertFileContent(basedir.resolve(path)); + } + + void create(Path basedir) { + content.createFile(basedir.resolve(path)); + } + } + + public record TestCopySpec(List pathGroupSpecs, Collection src, Collection dst) { + public TestCopySpec { + pathGroupSpecs.forEach(Objects::requireNonNull); + src.forEach(Objects::requireNonNull); + dst.forEach(Objects::requireNonNull); + } + + PathGroup from() { + return createPathGroup(PathGroupCopy::from); + } + + PathGroup to() { + return createPathGroup(PathGroupCopy::to); + } + + void test(Path tempDir) throws IOException { + final Path srcDir = tempDir.resolve("src"); + final Path dstDir = tempDir.resolve("dst"); + + src.stream().forEach(testFile -> { + testFile.create(srcDir); + }); + + from().resolveAt(srcDir).copy(to().resolveAt(dstDir)); + + dst.stream().forEach(testFile -> { + testFile.assertFileContent(dstDir); + }); + + Files.createDirectories(dstDir); + final var actualFiles = Stream.of(walkFiles(dstDir)).filter(path -> { + return Files.isRegularFile(dstDir.resolve(path)); + }).collect(toSet()); + final var expectedFiles = dst.stream().map(TestFile::path).collect(toSet()); + + assertEquals(expectedFiles, actualFiles); + } + + static Builder build() { + return new Builder(); + } + + static class Builder { + Builder addPath(String from, String to) { + pathGroupSpecs.add(new PathGroupCopy(Path.of(from), Optional.ofNullable(to).map(Path::of).orElse(null))); + return this; + } + + Builder file(TestFileContent content, String srcPath, String ...dstPaths) { + srcFiles.putAll(Map.of(Path.of(srcPath), content)); + dstFiles.putAll(Stream.of(dstPaths).collect(toMap(Path::of, x -> content))); + return this; + } + + TestCopySpec create() { + return new TestCopySpec(pathGroupSpecs, convert(srcFiles), convert(dstFiles)); + } + + private static Collection convert(Map map) { + return map.entrySet().stream().map(e -> { + return new TestFile(e.getKey(), e.getValue()); + }).toList(); + } + + private final List pathGroupSpecs = new ArrayList<>(); + private final Map srcFiles = new HashMap<>(); + private final Map dstFiles = new HashMap<>(); + } + + private PathGroup createPathGroup(Function keyFunc) { + return new PathGroup(IntStream.range(0, pathGroupSpecs.size()).mapToObj(Integer::valueOf).map(i -> { + return Optional.ofNullable(keyFunc.apply(pathGroupSpecs.get(i))).map(path -> { + return Map.entry(i, path); + }); + }).filter(Optional::isPresent).map(Optional::orElseThrow).collect(toMap(Map.Entry::getKey, Map.Entry::getValue))); + } + } + + @ParameterizedTest + @MethodSource + public void testCopy(TestCopySpec testSpec, @TempDir Path tempDir) throws IOException { + testSpec.test(tempDir); + } + + private static Collection testCopy() { + return List.of( + TestCopySpec.build().create(), + TestCopySpec.build() + .addPath("a/b/c", "a") + .file(TestFileContent.A, "a/b/c/j/k/foo", "a/j/k/foo") + .create(), + TestCopySpec.build() + .addPath("a/b/c", "a") + .addPath("a/b/c", "d") + .file(TestFileContent.A, "a/b/c/j/k/foo", "a/j/k/foo", "d/j/k/foo") + .create(), + TestCopySpec.build() + .addPath("a/b/c", "") + .addPath("a/b/c", "d") + .file(TestFileContent.A, "a/b/c/j/k/foo", "j/k/foo", "d/j/k/foo") + .create(), + TestCopySpec.build() + .addPath("a/b/c", "cc") + .addPath("a/b/c", "dd") + .addPath("a/b/c/foo", null) + .file(TestFileContent.A, "a/b/c/foo") + .create(), + TestCopySpec.build() + .addPath("a/b/c", "cc") + .addPath("a/b/c", "dd") + .addPath("a/b/c/foo", null) + .file(TestFileContent.A, "a/b/c/foo") + .file(TestFileContent.B, "a/b/c/bar", "cc/bar", "dd/bar") + .create(), + TestCopySpec.build() + .addPath("a/b/c", "cc") + .addPath("a/b/c", "dd") + .addPath("a/b/c/bar", "dd/buz") + .addPath("a/b/c/foo", null) + .file(TestFileContent.A, "a/b/c/foo") + .file(TestFileContent.B, "a/b/c/bar", "dd/buz") + .create(), + TestCopySpec.build() + .addPath("a/b/c", "cc") + .addPath("a/b/c", "dd") + .addPath("a/b/c/bar", "dd/buz") + .addPath("a/b/c/bar", "cc/rab") + .addPath("a/b/c/foo", null) + .file(TestFileContent.A, "a/b/c/foo") + .file(TestFileContent.B, "a/b/c/bar", "cc/rab", "dd/buz") + .create(), + TestCopySpec.build() + .addPath("a/b", null) + .addPath("a/b/c", "cc") + .addPath("a/b/c", "dd") + .addPath("a/b/c/bar", "dd/buz") + .addPath("a/b/c/bar", "cc/rab") + .file(TestFileContent.A, "a/b/c/foo") + .file(TestFileContent.B, "a/b/c/bar") + .create() + ); + } + private static Path[] walkFiles(Path root) throws IOException { try (var files = Files.walk(root)) { return files.map(root::relativize).sorted().toArray(Path[]::new); diff --git a/test/jdk/tools/jpackage/linux/UsrTreeTest.java b/test/jdk/tools/jpackage/linux/UsrTreeTest.java index e33364c605d..1950aab39a3 100644 --- a/test/jdk/tools/jpackage/linux/UsrTreeTest.java +++ b/test/jdk/tools/jpackage/linux/UsrTreeTest.java @@ -21,15 +21,18 @@ * questions. */ +import static java.util.stream.Collectors.toMap; +import static jdk.jpackage.test.ApplicationLayout.linuxAppImage; + import java.nio.file.Path; import java.util.List; import java.util.function.Consumer; -import java.util.stream.Collectors; -import jdk.jpackage.test.TKit; +import java.util.stream.Stream; +import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.LinuxHelper; import jdk.jpackage.test.PackageTest; import jdk.jpackage.test.PackageType; -import jdk.jpackage.test.LinuxHelper; -import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.TKit; /** @@ -90,20 +93,13 @@ public class UsrTreeTest { "Check there is%spackage name [%s] in common path [%s] between [%s] and [%s]", expectedImageSplit ? " no " : " ", packageName, commonPath, launcherPath, launcherCfgPath)); - - List packageFiles = LinuxHelper.getPackageFiles(cmd).collect( - Collectors.toList()); - - Consumer packageFileVerifier = file -> { - TKit.assertTrue(packageFiles.stream().filter( - path -> path.equals(file)).findFirst().orElse( - null) != null, String.format( - "Check file [%s] is in [%s] package", file, - packageName)); - }; - - packageFileVerifier.accept(launcherPath); - packageFileVerifier.accept(launcherCfgPath); + }) + .addInstallVerifier(cmd -> { + Stream.of( + cmd.appLauncherPath(), + cmd.appLauncherCfgPath(null), + cmd.appLayout().libapplauncher() + ).map(cmd::pathToPackageFile).map(cmd.appInstallationDirectory()::relativize).forEachOrdered(cmd::assertFileInAppImage); }) .run(); }