8356219: jpackage places libapplauncher.so in incorrect location in the app image

Reviewed-by: almatvee
This commit is contained in:
Alexey Semenyuk 2025-05-06 17:57:23 +00:00
parent 064b036ebd
commit 9c43871fd1
8 changed files with 782 additions and 205 deletions

View File

@ -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<ApplicationLayout> {
public final class ApplicationLayout {
enum PathRole {
/**
* Java run-time directory.
@ -84,16 +86,34 @@ public final class ApplicationLayout implements PathGroup.Facade<ApplicationLayo
this.data = data;
}
@Override
public PathGroup pathGroup() {
return data;
}
@Override
public ApplicationLayout resolveAt(Path root) {
return new ApplicationLayout(pathGroup().resolveAt(root));
}
public List<Path> 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.
*/

View File

@ -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<Object, Path> 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
* <code>null</code> 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 <code>null</code> 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 <code>null</code>
*/
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.
* <p>
* The order of identifiers in the returned list is undefined.
*
* @return all identifiers of this path group
*/
public Set<Object> keys() {
return entries.keySet();
}
/**
* Gets paths associated with all identifiers in this path group.
* <p>
* The order of paths in the returned list is undefined.
*
* @return paths associated with all identifiers in this path group
*/
public List<Path> paths() {
return entries.values().stream().toList();
}
/**
* Root entries.
* Gets root paths in this path group.
* <p>
* 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.
* <p>
* The order of paths in the returned list is undefined.
*
* @return unique root paths in this path group
*/
public List<Path> roots() {
// Sort by the number of path components in ascending order.
List<Map.Entry<Path, Path>> 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<Path, Path>, Map.Entry<Path, Path>, 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<Path> 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<Path> 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<Path> 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 <code>root.resolve()</code> 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<T> {
PathGroup pathGroup();
default Collection<Path> paths() {
return pathGroup().paths();
}
default List<Path> roots() {
return pathGroup().roots();
}
default long sizeInBytes() throws IOException {
return pathGroup().sizeInBytes();
}
T resolveAt(Path root);
default void copy(Facade<T> dst) throws IOException {
pathGroup().copy(dst.pathGroup());
}
default void move(Facade<T> dst) throws IOException {
pathGroup().move(dst.pathGroup());
}
default void transform(Facade<T> 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<Map.Entry<Path, Path>> copyItems = new ArrayList<>();
List<Path> 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<Map.Entry<Path, Path>> entries,
List<Path> excludePaths, TransformHandler handler) throws
IOException {
private static void copy(PathGroup src, PathGroup dst, TransformHandler handler, boolean followSymlinks) throws IOException {
List<CopySpec> copySpecs = new ArrayList<>();
List<Path> 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<Path> paths) {
return paths.stream().anyMatch(what::startsWith);
}
private static void copy(List<CopySpec> copySpecs, List<Path> excludePaths,
TransformHandler handler, boolean followSymlinks) throws IOException {
Objects.requireNonNull(excludePaths);
Objects.requireNonNull(handler);
final var filteredCopySpecs = copySpecs.stream().<CopySpec>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<Path, Path> actions = new HashMap<>();
for (var action: entries) {
Path src = action.getKey();
Path dst = action.getValue();
if (Files.isDirectory(src)) {
try (Stream<Path> 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<Path, Path> 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<Map.Entry<Path, Path>> normalizedPaths() {
return entries.values().stream().map(PathGroup::normalizedPath).collect(
Collectors.toList());
}
private final Map<Object, Path> entries;

View File

@ -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")
);
}

View File

@ -1008,9 +1008,9 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
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<JPackageCommand> {
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;

View File

@ -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()) {

View File

@ -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<Path> desktopFiles = files
.filter(path -> path.getFileName().toString().endsWith(".desktop"))
.toList();
@ -470,7 +470,7 @@ public final class LinuxHelper {
for (var e : List.<Map.Entry<Map.Entry<String, Optional<String>>, Function<ApplicationLayout, Path>>>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<Scriptlet, List<String>> scriptlets = getScriptlets(cmd);

View File

@ -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<Path> paths, Set<Path> 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<String> paths, Set<String> 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<TestRootsSpec> 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<Path> 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<List<Path>> 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<Object[]> 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<PathRole, List<Path>> 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<PathGroupCopy> pathGroupSpecs, Collection<TestFile> src, Collection<TestFile> 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<TestFile> convert(Map<Path, TestFileContent> map) {
return map.entrySet().stream().map(e -> {
return new TestFile(e.getKey(), e.getValue());
}).toList();
}
private final List<PathGroupCopy> pathGroupSpecs = new ArrayList<>();
private final Map<Path, TestFileContent> srcFiles = new HashMap<>();
private final Map<Path, TestFileContent> dstFiles = new HashMap<>();
}
private PathGroup createPathGroup(Function<PathGroupCopy, Path> 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<TestCopySpec> 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);

View File

@ -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<Path> packageFiles = LinuxHelper.getPackageFiles(cmd).collect(
Collectors.toList());
Consumer<Path> 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();
}