8325089: jpackage utility creates an "infinite", undeleteable directory tree

Reviewed-by: almatvee
This commit is contained in:
Alexey Semenyuk 2024-10-31 20:25:55 +00:00
parent 7c36fa7e17
commit 568b07a09b
15 changed files with 835 additions and 88 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2015, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2015, 2024, 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
@ -27,14 +27,20 @@ package jdk.jpackage.internal;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Map;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
import static jdk.jpackage.internal.OverridableResource.createResource;
import static jdk.jpackage.internal.StandardBundlerParam.APP_NAME;
import static jdk.jpackage.internal.StandardBundlerParam.ICON;
import static jdk.jpackage.internal.StandardBundlerParam.SOURCE_DIR;
import static jdk.jpackage.internal.StandardBundlerParam.APP_CONTENT;
import static jdk.jpackage.internal.StandardBundlerParam.OUTPUT_DIR;
import static jdk.jpackage.internal.StandardBundlerParam.TEMP_ROOT;
import jdk.jpackage.internal.resources.ResourceLocator;
/*
@ -73,8 +79,21 @@ public abstract class AbstractAppImageBuilder {
throws IOException {
Path inputPath = SOURCE_DIR.fetchFrom(params);
if (inputPath != null) {
IOUtils.copyRecursive(SOURCE_DIR.fetchFrom(params),
appLayout.appDirectory());
inputPath = inputPath.toAbsolutePath();
List<Path> excludes = new ArrayList<>();
for (var path : List.of(TEMP_ROOT.fetchFrom(params), OUTPUT_DIR.fetchFrom(params), root)) {
if (Files.isDirectory(path)) {
path = path.toAbsolutePath();
if (path.startsWith(inputPath) && !Files.isSameFile(path, inputPath)) {
excludes.add(path);
}
}
}
IOUtils.copyRecursive(inputPath,
appLayout.appDirectory().toAbsolutePath(), excludes);
}
AppImageFile.save(root, params);

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2018, 2024, 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
@ -89,9 +89,6 @@ public class Arguments {
private List<CLIOptions> allOptions = null;
private String input = null;
private Path output = null;
private boolean hasMainJar = false;
private boolean hasMainClass = false;
private boolean hasMainModule = false;
@ -135,9 +132,6 @@ public class Arguments {
allOptions = new ArrayList<>();
addLaunchers = new ArrayList<>();
output = Paths.get("").toAbsolutePath();
deployParams.setOutput(output);
}
// CLIOptions is public for DeployParamsTest
@ -147,13 +141,12 @@ public class Arguments {
}),
INPUT ("input", "i", OptionCategories.PROPERTY, () -> {
context().input = popArg();
setOptionValue("input", context().input);
setOptionValue("input", popArg());
}),
OUTPUT ("dest", "d", OptionCategories.PROPERTY, () -> {
context().output = Path.of(popArg());
context().deployParams.setOutput(context().output);
var path = Path.of(popArg());
setOptionValue("dest", path);
}),
DESCRIPTION ("description", OptionCategories.PROPERTY),
@ -711,7 +704,8 @@ public class Arguments {
Map<String, ? super Object> localParams = new HashMap<>(params);
try {
bundler.validate(localParams);
Path result = bundler.execute(localParams, deployParams.outdir);
Path result = bundler.execute(localParams,
StandardBundlerParam.OUTPUT_DIR.fetchFrom(params));
if (result == null) {
throw new PackagerException("MSG_BundlerFailed",
bundler.getID(), bundler.getName());

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2011, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2011, 2024, 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
@ -49,15 +49,9 @@ public class DeployParams {
String targetFormat = null; // means default type for this platform
Path outdir = null;
// raw arguments to the bundler
Map<String, ? super Object> bundlerArguments = new LinkedHashMap<>();
public void setOutput(Path output) {
outdir = output;
}
static class Template {
Path in;
Path out;

View File

@ -121,29 +121,52 @@ public class IOUtils {
}
public static void copyRecursive(Path src, Path dest,
final List<String> excludes, CopyOption... options)
final List<Path> excludes, CopyOption... options)
throws IOException {
List<CopyAction> copyActions = new ArrayList<>();
Files.walkFileTree(src, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(final Path dir,
final BasicFileAttributes attrs) throws IOException {
if (excludes.contains(dir.toFile().getName())) {
final BasicFileAttributes attrs) {
if (isPathMatch(dir, excludes)) {
return FileVisitResult.SKIP_SUBTREE;
} else {
Files.createDirectories(dest.resolve(src.relativize(dir)));
copyActions.add(new CopyAction(null, dest.resolve(src.
relativize(dir))));
return FileVisitResult.CONTINUE;
}
}
@Override
public FileVisitResult visitFile(final Path file,
final BasicFileAttributes attrs) throws IOException {
if (!excludes.contains(file.toFile().getName())) {
Files.copy(file, dest.resolve(src.relativize(file)), options);
final BasicFileAttributes attrs) {
if (!isPathMatch(file, excludes)) {
copyActions.add(new CopyAction(file, dest.resolve(src.
relativize(file))));
}
return FileVisitResult.CONTINUE;
}
});
for (var copyAction : copyActions) {
copyAction.apply(options);
}
}
private static record CopyAction(Path src, Path dest) {
void apply(CopyOption... options) throws IOException {
if (src == null) {
Files.createDirectories(dest);
} else {
Files.copy(src, dest, options);
}
}
}
private static boolean isPathMatch(Path what, List<Path> paths) {
return paths.stream().anyMatch(what::endsWith);
}
public static void copyFile(Path sourceFile, Path destFile)

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2014, 2023, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2014, 2024, 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
@ -43,7 +43,6 @@ import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
@ -100,6 +99,14 @@ class StandardBundlerParam<T> extends BundlerParamInfo<T> {
(s, p) -> Path.of(s)
);
static final StandardBundlerParam<Path> OUTPUT_DIR =
new StandardBundlerParam<>(
Arguments.CLIOptions.OUTPUT.getId(),
Path.class,
p -> Path.of("").toAbsolutePath(),
(s, p) -> Path.of(s)
);
// note that each bundler is likely to replace this one with
// their own converter
static final StandardBundlerParam<Path> MAIN_JAR =
@ -596,7 +603,7 @@ class StandardBundlerParam<T> extends BundlerParamInfo<T> {
}
// copy whole runtime, need to skip jmods and src.zip
final List<String> excludes = Arrays.asList("jmods", "src.zip");
final List<Path> excludes = Arrays.asList(Path.of("jmods"), Path.of("src.zip"));
IOUtils.copyRecursive(topImage, appLayout.runtimeHomeDirectory(),
excludes, LinkOption.NOFOLLOW_LINKS);

View File

@ -0,0 +1,171 @@
/*
* Copyright (c) 2024, 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.test;
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.List;
import java.util.Set;
import java.util.function.BiConsumer;
import static java.util.stream.Collectors.toSet;
import java.util.stream.Stream;
import jdk.jpackage.test.Annotations.Parameters;
import jdk.jpackage.test.Annotations.Test;
import static jdk.jpackage.test.DirectoryContentVerifierTest.AssertType.CONTAINS;
import static jdk.jpackage.test.DirectoryContentVerifierTest.AssertType.MATCH;
import jdk.jpackage.test.TKit.DirectoryContentVerifier;
import static jdk.jpackage.test.TKit.assertAssert;
/*
* @test
* @summary Test TKit.DirectoryContentVerifier from jpackage test library
* @library /test/jdk/tools/jpackage/helpers
* @build jdk.jpackage.test.*
* @modules jdk.jpackage/jdk.jpackage.internal
* @compile DirectoryContentVerifierTest.java
* @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main
* --jpt-run=jdk.jpackage.test.DirectoryContentVerifierTest
*/
public class DirectoryContentVerifierTest {
enum AssertType {
MATCH(DirectoryContentVerifier::match),
CONTAINS(DirectoryContentVerifier::contains),
;
AssertType(BiConsumer<DirectoryContentVerifier, Set<Path>> assertFunc) {
this.assertFunc = assertFunc;
}
private final BiConsumer<DirectoryContentVerifier, Set<Path>> assertFunc;
}
private static ArgsBuilder buildArgs() {
return new ArgsBuilder();
}
private static class ArgsBuilder {
void applyTo(List<Object[]> data) {
data.add(new Object[]{expectedPaths, actualPaths, assertOp, success});
}
void applyVariantsTo(List<Object[]> data) {
applyTo(data);
boolean pathGroupsEqual = List.of(expectedPaths).equals(List.of(actualPaths));
if (assertOp == MATCH) {
if (!pathGroupsEqual) {
data.add(new Object[]{actualPaths, expectedPaths, MATCH, success});
}
if (success) {
data.add(new Object[]{expectedPaths, actualPaths, CONTAINS, success});
if (!pathGroupsEqual) {
data.add(new Object[]{actualPaths, expectedPaths, CONTAINS, success});
}
}
}
}
ArgsBuilder expectedPaths(String... paths) {
expectedPaths = paths;
return this;
}
ArgsBuilder actualPaths(String... paths) {
actualPaths = paths;
return this;
}
ArgsBuilder assertOp(AssertType v) {
assertOp = v;
return this;
}
ArgsBuilder expectFail() {
success = false;
return this;
}
private String[] expectedPaths = new String[0];
private String[] actualPaths = new String[0];
private AssertType assertOp = MATCH;
private boolean success = true;
}
@Parameters
public static Collection input() {
List<Object[]> data = new ArrayList<>();
buildArgs().applyVariantsTo(data);
buildArgs().actualPaths("foo").assertOp(CONTAINS).applyTo(data);
buildArgs().actualPaths("zoo").expectFail().applyVariantsTo(data);
buildArgs().actualPaths("boo").expectedPaths("boo").applyVariantsTo(data);
if (TKit.isWindows()) {
buildArgs().actualPaths("moo").expectedPaths("Moo").applyVariantsTo(data);
} else {
buildArgs().actualPaths("moo").expectedPaths("Moo").expectFail().applyVariantsTo(data);
}
buildArgs().actualPaths("hello").expectedPaths().expectFail().applyVariantsTo(data);
buildArgs().actualPaths("123").expectedPaths("456").expectFail().applyVariantsTo(data);
buildArgs().actualPaths("a", "b", "c").expectedPaths("b", "a", "c").applyVariantsTo(data);
buildArgs().actualPaths("AA", "BB", "CC").expectedPaths("BB", "AA").expectFail().applyVariantsTo(data);
buildArgs().actualPaths("AA", "BB", "CC").expectedPaths("BB", "AA").assertOp(CONTAINS).applyTo(data);
buildArgs().actualPaths("AA", "BB", "CC").expectedPaths("BB", "DD", "AA").expectFail().assertOp(CONTAINS).applyTo(data);
buildArgs().actualPaths("AA", "BB", "CC").expectedPaths("BB", "DD", "AA").expectFail().applyTo(data);
return data;
}
public DirectoryContentVerifierTest(String[] expectedPaths, String[] actualPaths,
AssertType assertOp, Boolean success) {
this.expectedPaths = conv(expectedPaths);
this.actualPaths = conv(actualPaths);
this.assertOp = assertOp;
this.success = success;
}
@Test
public void test() {
TKit.withTempDirectory("basedir", this::test);
}
private void test(Path basedir) throws IOException {
for (var path : actualPaths) {
Files.createFile(basedir.resolve(path));
}
var testee = TKit.assertDirectoryContent(basedir);
assertAssert(success, () -> assertOp.assertFunc.accept(testee, expectedPaths));
}
private static Set<Path> conv(String... paths) {
return Stream.of(paths).map(Path::of).collect(toSet());
}
private final Set<Path> expectedPaths;
private final Set<Path> actualPaths;
private final AssertType assertOp;
private final boolean success;
}

View File

@ -34,30 +34,35 @@ public class CommandArguments<T> {
args = new ArrayList<>();
}
final public T addArgument(String v) {
public final T clearArguments() {
args.clear();
return (T) this;
}
public final T addArgument(String v) {
args.add(v);
return (T) this;
}
final public T addArguments(List<String> v) {
public final T addArguments(List<String> v) {
args.addAll(v);
return (T) this;
}
final public T addArgument(Path v) {
public final T addArgument(Path v) {
return addArgument(v.toString());
}
final public T addArguments(String... v) {
public final T addArguments(String... v) {
return addArguments(Arrays.asList(v));
}
final public T addPathArguments(List<Path> v) {
public final T addPathArguments(List<Path> v) {
return addArguments(v.stream().map((p) -> p.toString()).collect(
Collectors.toList()));
}
final public List<String> getAllArguments() {
public final List<String> getAllArguments() {
return List.copyOf(args);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2023, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2024, 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
@ -52,6 +52,7 @@ import jdk.jpackage.internal.PackageFile;
import static jdk.jpackage.test.AdditionalLauncher.forEachAdditionalLauncher;
import jdk.jpackage.test.Functional.ThrowingConsumer;
import jdk.jpackage.test.Functional.ThrowingFunction;
import jdk.jpackage.test.Functional.ThrowingRunnable;
import jdk.jpackage.test.Functional.ThrowingSupplier;
/**
@ -76,6 +77,8 @@ public final class JPackageCommand extends CommandArguments<JPackageCommand> {
immutable = cmd.immutable;
prerequisiteActions = new Actions(cmd.prerequisiteActions);
verifyActions = new Actions(cmd.verifyActions);
appLayoutAsserts = cmd.appLayoutAsserts;
executeInDirectory = cmd.executeInDirectory;
}
JPackageCommand createImmutableCopy() {
@ -198,7 +201,10 @@ public final class JPackageCommand extends CommandArguments<JPackageCommand> {
}
public Path outputDir() {
return getArgumentValue("--dest", () -> Path.of("."), Path::of);
var path = getArgumentValue("--dest", () -> Path.of("."), Path::of);
return Optional.ofNullable(executeInDirectory).map(base -> {
return base.resolve(path);
}).orElse(path);
}
public Path inputDir() {
@ -691,6 +697,12 @@ public final class JPackageCommand extends CommandArguments<JPackageCommand> {
return this;
}
public JPackageCommand setDirectory(Path v) {
verifyMutable();
executeInDirectory = v;
return this;
}
public JPackageCommand saveConsoleOutput(boolean v) {
verifyMutable();
saveConsoleOutput = v;
@ -733,6 +745,7 @@ public final class JPackageCommand extends CommandArguments<JPackageCommand> {
private Executor createExecutor() {
Executor exec = new Executor()
.saveOutput(saveConsoleOutput).dumpOutput(!suppressOutput)
.setDirectory(executeInDirectory)
.addArguments(args);
if (isWithToolProvider()) {
@ -755,18 +768,19 @@ public final class JPackageCommand extends CommandArguments<JPackageCommand> {
executePrerequisiteActions();
if (hasArgument("--dest")) {
if (isImagePackageType()) {
TKit.deleteDirectoryContentsRecursive(outputDir());
} else {
nullableOutputBundle().ifPresent(path -> {
if (ThrowingSupplier.toSupplier(() -> TKit.deleteIfExists(
path)).get()) {
nullableOutputBundle().ifPresent(path -> {
ThrowingRunnable.toRunnable(() -> {
if (Files.isDirectory(path)) {
TKit.deleteDirectoryRecursive(path, String.format(
"Delete [%s] folder before running jpackage",
path));
} else if (TKit.deleteIfExists(path)) {
TKit.trace(String.format(
"Deleted [%s] file before running jpackage",
path));
}
});
}
}).run();
});
}
Path resourceDir = getArgumentValue("--resource-dir", () -> null, Path::of);
@ -816,22 +830,69 @@ public final class JPackageCommand extends CommandArguments<JPackageCommand> {
return this;
}
JPackageCommand assertAppLayout() {
assertAppImageFile();
assertPackageFile();
TKit.assertDirectoryExists(appRuntimeDirectory());
if (!isRuntime()) {
TKit.assertExecutableFileExists(appLauncherPath());
TKit.assertFileExists(appLauncherCfgPath(null));
if (TKit.isOSX()) {
TKit.assertFileExists(appRuntimeDirectory().resolve(
"Contents/MacOS/libjli.dylib"));
public static enum AppLayoutAssert {
APP_IMAGE_FILE(JPackageCommand::assertAppImageFile),
PACKAGE_FILE(JPackageCommand::assertPackageFile),
MAIN_LAUNCHER(cmd -> {
if (cmd.isRuntime()) {
TKit.assertPathExists(convertFromRuntime(cmd).appLauncherPath(), false);
} else {
TKit.assertExecutableFileExists(cmd.appLauncherPath());
}
}),
MAIN_LAUNCHER_CFG_FILE(cmd -> {
if (cmd.isRuntime()) {
TKit.assertPathExists(convertFromRuntime(cmd).appLauncherCfgPath(null), false);
} else {
TKit.assertFileExists(cmd.appLauncherCfgPath(null));
}
}),
RUNTIME_DIRECTORY(cmd -> {
TKit.assertDirectoryExists(cmd.appRuntimeDirectory());
if (TKit.isOSX()) {
var libjliPath = cmd.appRuntimeDirectory().resolve("Contents/MacOS/libjli.dylib");
if (cmd.isRuntime()) {
TKit.assertPathExists(libjliPath, false);
} else {
TKit.assertFileExists(libjliPath);
}
}
}),
MAC_BUNDLE_STRUCTURE(cmd -> {
if (TKit.isOSX()) {
MacHelper.verifyBundleStructure(cmd);
}
}),
;
AppLayoutAssert(Consumer<JPackageCommand> action) {
this.action = action;
}
private static JPackageCommand convertFromRuntime(JPackageCommand cmd) {
var copy = new JPackageCommand(cmd);
copy.immutable = false;
copy.removeArgumentWithValue("--runtime-image");
return copy;
}
private final Consumer<JPackageCommand> action;
}
public JPackageCommand setAppLayoutAsserts(AppLayoutAssert ... asserts) {
appLayoutAsserts = Set.of(asserts);
return this;
}
public JPackageCommand excludeAppLayoutAsserts(AppLayoutAssert... asserts) {
return setAppLayoutAsserts(Stream.of(asserts).filter(Predicate.not(
appLayoutAsserts::contains)).toArray(AppLayoutAssert[]::new));
}
JPackageCommand assertAppLayout() {
for (var appLayoutAssert : appLayoutAsserts.stream().sorted().toList()) {
appLayoutAssert.action.accept(this);
}
return this;
}
@ -1111,9 +1172,11 @@ public final class JPackageCommand extends CommandArguments<JPackageCommand> {
private boolean immutable;
private final Actions prerequisiteActions;
private final Actions verifyActions;
private Path executeInDirectory;
private Set<AppLayoutAssert> appLayoutAsserts = Set.of(AppLayoutAssert.values());
private static boolean defaultWithToolProvider;
private final static Map<String, PackageType> PACKAGE_TYPES = Functional.identity(
private static final Map<String, PackageType> PACKAGE_TYPES = Functional.identity(
() -> {
Map<String, PackageType> reply = new HashMap<>();
for (PackageType type : PackageType.values()) {
@ -1122,7 +1185,7 @@ public final class JPackageCommand extends CommandArguments<JPackageCommand> {
return reply;
}).get();
public final static Path DEFAULT_RUNTIME_IMAGE = Functional.identity(() -> {
public static final Path DEFAULT_RUNTIME_IMAGE = Functional.identity(() -> {
// Set the property to the path of run-time image to speed up
// building app images and platform bundles by avoiding running jlink
// The value of the property will be automativcally appended to
@ -1135,5 +1198,5 @@ public final class JPackageCommand extends CommandArguments<JPackageCommand> {
return null;
}).get();
private final static String UNPACKED_PATH_ARGNAME = "jpt-unpacked-folder";
private static final String UNPACKED_PATH_ARGNAME = "jpt-unpacked-folder";
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2024, 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
@ -216,7 +216,9 @@ public final class JavaAppDesc {
components[0].length() - 1);
desc.setWithMainClass(true);
}
desc.setClassName(components[0]);
if (!components[0].isEmpty()) {
desc.setClassName(components[0]);
}
if (components.length == 2) {
desc.setModuleVersion(components[1]);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2023, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2024, 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
@ -35,6 +35,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.toSet;
import java.util.stream.Stream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
@ -257,6 +258,20 @@ public final class MacHelper {
return pkg;
}
static void verifyBundleStructure(JPackageCommand cmd) {
Path bundleRoot;
if (cmd.isImagePackageType()) {
bundleRoot = cmd.outputBundle();
} else {
bundleRoot = cmd.pathToUnpackedPackageFile(
cmd.appInstallationDirectory());
}
TKit.assertDirectoryContent(bundleRoot).match(Path.of("Contents"));
TKit.assertDirectoryContent(bundleRoot.resolve("Contents")).match(
cmd.isRuntime() ? RUNTIME_BUNDLE_CONTENTS : APP_BUNDLE_CONTENTS);
}
static String getBundleName(JPackageCommand cmd) {
cmd.verifyIsOfType(PackageType.MAC);
return String.format("%s-%s%s", getPackageName(cmd), cmd.version(),
@ -390,5 +405,19 @@ public final class MacHelper {
static final Set<Path> CRITICAL_RUNTIME_FILES = Set.of(Path.of(
"Contents/Home/lib/server/libjvm.dylib"));
private final static Method getServicePListFileName = initGetServicePListFileName();
private static final Method getServicePListFileName = initGetServicePListFileName();
private static final Set<Path> APP_BUNDLE_CONTENTS = Stream.of(
"Info.plist",
"MacOS",
"app",
"runtime",
"Resources",
"PkgInfo",
"_CodeSignature"
).map(Path::of).collect(toSet());
private static final Set<Path> RUNTIME_BUNDLE_CONTENTS = Stream.of(
"Home"
).map(Path::of).collect(toSet());
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2023, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2024, 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
@ -127,6 +127,15 @@ public final class PackageTest extends RunnablePackageTest {
return this;
}
public PackageTest ignoreBundleOutputDir() {
return ignoreBundleOutputDir(true);
}
public PackageTest ignoreBundleOutputDir(boolean v) {
ignoreBundleOutputDir = v;
return this;
}
private PackageTest addInitializer(ThrowingConsumer<JPackageCommand> v,
String id) {
if (id != null) {
@ -368,7 +377,7 @@ public final class PackageTest extends RunnablePackageTest {
private final List<Consumer<Action>> handlers;
}
final static class PackageHandlers {
static final class PackageHandlers {
Consumer<JPackageCommand> installHandler;
Consumer<JPackageCommand> uninstallHandler;
BiFunction<JPackageCommand, Path, Path> unpackHandler;
@ -528,7 +537,7 @@ public final class PackageTest extends RunnablePackageTest {
private final JPackageCommand cmd = Functional.identity(() -> {
JPackageCommand result = new JPackageCommand();
result.setDefaultInputOutput().setDefaultAppName();
if (BUNDLE_OUTPUT_DIR != null) {
if (BUNDLE_OUTPUT_DIR != null && !ignoreBundleOutputDir) {
result.setArgumentValue("--dest", BUNDLE_OUTPUT_DIR.toString());
}
type.applyTo(result);
@ -777,8 +786,9 @@ public final class PackageTest extends RunnablePackageTest {
private Map<PackageType, Handler> handlers;
private Set<String> namedInitializers;
private Map<PackageType, PackageHandlers> packageHandlers;
private boolean ignoreBundleOutputDir;
private final static File BUNDLE_OUTPUT_DIR;
private static final File BUNDLE_OUTPUT_DIR;
static {
final String propertyName = "output";

View File

@ -40,10 +40,12 @@ import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@ -57,13 +59,14 @@ import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.toSet;
import java.util.stream.Stream;
import jdk.jpackage.test.Functional.ExceptionBox;
import jdk.jpackage.test.Functional.ThrowingConsumer;
import jdk.jpackage.test.Functional.ThrowingRunnable;
import jdk.jpackage.test.Functional.ThrowingSupplier;
final public class TKit {
public final class TKit {
private static final String OS = System.getProperty("os.name").toLowerCase();
@ -84,7 +87,7 @@ final public class TKit {
return TEST_SRC_ROOT.resolve("../../../../src/jdk.jpackage").normalize().toAbsolutePath();
}).get();
public final static String ICON_SUFFIX = Functional.identity(() -> {
public static final String ICON_SUFFIX = Functional.identity(() -> {
if (isOSX()) {
return ".icns";
}
@ -273,7 +276,23 @@ final public class TKit {
throw new AssertionError(v);
}
private final static String TEMP_FILE_PREFIX = null;
static void assertAssert(boolean expectedSuccess, Runnable runnable) {
try {
runnable.run();
} catch (AssertionError err) {
if (expectedSuccess) {
assertUnexpected("Assertion failed");
} else {
return;
}
}
if (!expectedSuccess) {
assertUnexpected("Assertion passed");
}
}
private static final String TEMP_FILE_PREFIX = null;
private static Path createUniqueFileName(String defaultName) {
final String[] nameComponents;
@ -740,6 +759,101 @@ final public class TKit {
error(concatMessages("Unexpected", msg));
}
public static DirectoryContentVerifier assertDirectoryContent(Path dir) {
return new DirectoryContentVerifier(dir);
}
public static final class DirectoryContentVerifier {
public DirectoryContentVerifier(Path baseDir) {
this(baseDir, ThrowingSupplier.toSupplier(() -> {
try (var files = Files.list(baseDir)) {
return files.map(Path::getFileName).collect(toSet());
}
}).get());
}
public void match(Path ... expected) {
DirectoryContentVerifier.this.match(Set.of(expected));
}
public void match(Set<Path> expected) {
currentTest.notifyAssert();
var comm = Comm.compare(content, expected);
if (!comm.unique1.isEmpty() && !comm.unique2.isEmpty()) {
error(String.format(
"assertDirectoryContentEquals(%s): Some expected %s. Unexpected %s. Missing %s",
baseDir, format(comm.common), format(comm.unique1), format(comm.unique2)));
} else if (!comm.unique1.isEmpty()) {
error(String.format(
"assertDirectoryContentEquals%s: Expected %s. Unexpected %s",
baseDir, format(comm.common), format(comm.unique1)));
} else if (!comm.unique2.isEmpty()) {
error(String.format(
"assertDirectoryContentEquals(%s): Some expected %s. Missing %s",
baseDir, format(comm.common), format(comm.unique2)));
} else {
traceAssert(String.format(
"assertDirectoryContentEquals(%s): Expected %s",
baseDir, format(expected)));
}
}
public void contains(Path ... expected) {
contains(Set.of(expected));
}
public void contains(Set<Path> expected) {
currentTest.notifyAssert();
var comm = Comm.compare(content, expected);
if (!comm.unique2.isEmpty()) {
error(String.format(
"assertDirectoryContentContains(%s): Some expected %s. Missing %s",
baseDir, format(comm.common), format(comm.unique2)));
} else {
traceAssert(String.format(
"assertDirectoryContentContains(%s): Expected %s",
baseDir, format(expected)));
}
}
public DirectoryContentVerifier removeAll(Path ... paths) {
Set<Path> newContent = new HashSet<>(content);
newContent.removeAll(List.of(paths));
return new DirectoryContentVerifier(baseDir, newContent);
}
private DirectoryContentVerifier(Path baseDir, Set<Path> contents) {
this.baseDir = baseDir;
this.content = contents;
}
private static record Comm(Set<Path> common, Set<Path> unique1, Set<Path> unique2) {
static Comm compare(Set<Path> a, Set<Path> b) {
Set<Path> common = new HashSet<>(a);
common.retainAll(b);
Set<Path> unique1 = new HashSet<>(a);
unique1.removeAll(common);
Set<Path> unique2 = new HashSet<>(b);
unique2.removeAll(common);
return new Comm(common, unique1, unique2);
}
}
private static String format(Set<Path> paths) {
return Arrays.toString(
paths.stream().sorted().map(Path::toString).toArray(
String[]::new));
}
private final Path baseDir;
private final Set<Path> content;
}
public static void assertStringListEquals(List<String> expected,
List<String> actual, String msg) {
currentTest.notifyAssert();
@ -817,7 +931,7 @@ final public class TKit {
};
}
public final static class TextStreamVerifier {
public static final class TextStreamVerifier {
TextStreamVerifier(String value) {
this.value = value;
predicate(String::contains);
@ -880,7 +994,7 @@ final public class TKit {
private String label;
private boolean negate;
private Supplier<RuntimeException> createException;
final private String value;
private final String value;
}
public static TextStreamVerifier assertTextStream(String what) {

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2018, 2024, 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
@ -22,12 +22,9 @@
*/
package jdk.jpackage.internal;
import java.nio.file.Path;
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;
@ -40,11 +37,6 @@ public class DeployParamsTest {
@Rule
public final ExpectedException thrown = ExpectedException.none();
@Before
public void setUp() throws IOException {
testRoot = tempFolder.newFolder().toPath();
}
@Test
public void testValidAppName() throws PackagerException {
initParamsAppName();
@ -115,7 +107,6 @@ public class DeployParamsTest {
private void initParamsAppName() {
params = new DeployParams();
params.setOutput(testRoot);
params.addBundleArgument(Arguments.CLIOptions.APPCLASS.getId(),
"TestClass");
params.addBundleArgument(Arguments.CLIOptions.MAIN_JAR.getId(),
@ -128,6 +119,5 @@ public class DeployParamsTest {
params.validate();
}
private Path testRoot = null;
private DeployParams params;
}

View File

@ -0,0 +1,266 @@
/*
* Copyright (c) 2024, 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 java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import static java.util.stream.Collectors.toSet;
import java.util.stream.Stream;
import jdk.jpackage.internal.AppImageFile;
import jdk.jpackage.internal.ApplicationLayout;
import jdk.jpackage.internal.PackageFile;
import jdk.jpackage.test.Annotations;
import jdk.jpackage.test.Annotations.Test;
import jdk.jpackage.test.Functional.ThrowingConsumer;
import jdk.jpackage.test.JPackageCommand;
import jdk.jpackage.test.JPackageCommand.AppLayoutAssert;
import jdk.jpackage.test.PackageTest;
import jdk.jpackage.test.PackageType;
import static jdk.jpackage.test.RunnablePackageTest.Action.CREATE_AND_UNPACK;
import jdk.jpackage.test.TKit;
/*
* @test
* @summary Test jpackage command line with overlapping input and output paths
* @library ../helpers
* @build jdk.jpackage.test.*
* @modules jdk.jpackage/jdk.jpackage.internal
* @compile InOutPathTest.java
* @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main
* --jpt-run=InOutPathTest
*/
public final class InOutPathTest {
@Annotations.Parameters
public static Collection input() {
List<Object[]> data = new ArrayList<>();
for (var packageTypes : List.of(PackageType.IMAGE.toString(), ALL_NATIVE_PACKAGE_TYPES)) {
data.addAll(List.of(new Object[][]{
{packageTypes, wrap(InOutPathTest::outputDirInInputDir, "--dest in --input")},
{packageTypes, wrap(InOutPathTest::outputDirSameAsInputDir, "--dest same as --input")},
{packageTypes, wrap(InOutPathTest::tempDirInInputDir, "--temp in --input")},
{packageTypes, wrap(cmd -> {
outputDirInInputDir(cmd);
tempDirInInputDir(cmd);
}, "--dest and --temp in --input")},
}));
data.addAll(additionalContentInput(packageTypes, "--app-content"));
}
data.addAll(List.of(new Object[][]{
{PackageType.IMAGE.toString(), wrap(cmd -> {
additionalContent(cmd, "--app-content", cmd.outputBundle());
}, "--app-content same as output bundle")},
}));
if (TKit.isOSX()) {
data.addAll(additionalContentInput(PackageType.MAC_DMG.toString(),
"--mac-dmg-content"));
}
return data;
}
private static List<Object[]> additionalContentInput(String packageTypes, String argName) {
List<Object[]> data = new ArrayList<>();
data.addAll(List.of(new Object[][]{
{packageTypes, wrap(cmd -> {
additionalContent(cmd, argName, cmd.inputDir());
}, argName + " same as --input")},
}));
if (!TKit.isOSX()) {
data.addAll(List.of(new Object[][]{
{packageTypes, wrap(cmd -> {
additionalContent(cmd, argName, cmd.inputDir().resolve("foo"));
}, argName + " in --input")},
{packageTypes, wrap(cmd -> {
additionalContent(cmd, argName, cmd.outputDir().resolve("bar"));
}, argName + " in --dest")},
{packageTypes, wrap(cmd -> {
additionalContent(cmd, argName, cmd.outputDir());
}, argName + " same as --dest")},
{packageTypes, wrap(cmd -> {
tempDirInInputDir(cmd);
var tempDir = cmd.getArgumentValue("--temp");
Files.createDirectories(Path.of(tempDir));
cmd.addArguments(argName, tempDir);
}, argName + " as --temp; --temp in --input")},
}));
}
return data;
}
public InOutPathTest(String packageTypes, Envelope configure) {
if (ALL_NATIVE_PACKAGE_TYPES.equals(packageTypes)) {
this.packageTypes = PackageType.NATIVE;
} else {
this.packageTypes = Stream.of(packageTypes.split(",")).map(
PackageType::valueOf).collect(toSet());
}
this.configure = configure.value;
}
@Test
public void test() throws Throwable {
runTest(packageTypes, configure);
}
private static Envelope wrap(ThrowingConsumer<JPackageCommand> v, String label) {
return new Envelope(v, label);
}
private static boolean isAppImageValid(JPackageCommand cmd) {
return !cmd.hasArgument("--app-content") && !cmd.hasArgument("--mac-dmg-content");
}
private static void runTest(Set<PackageType> packageTypes,
ThrowingConsumer<JPackageCommand> configure) throws Throwable {
ThrowingConsumer<JPackageCommand> configureWrapper = cmd -> {
// Make sure the input directory is empty in every test run.
// This is needed because jpackage output directories in this test
// are subdirectories of the input directory.
cmd.setInputToEmptyDirectory();
configure.accept(cmd);
if (cmd.hasArgument("--temp") && cmd.isImagePackageType()) {
// Request to build app image wit user supplied temp directory,
// ignore external runtime if any to make use of the temp directory
// for runtime generation.
cmd.ignoreDefaultRuntime(true);
} else {
cmd.setFakeRuntime();
}
if (!isAppImageValid(cmd)) {
// Standard asserts for .jpackage.xml fail in messed up app image. Disable them.
// Other standard asserts for app image contents should pass.
cmd.excludeAppLayoutAsserts(AppLayoutAssert.APP_IMAGE_FILE);
}
};
if (packageTypes.contains(PackageType.IMAGE)) {
JPackageCommand cmd = JPackageCommand.helloAppImage(JAR_PATH.toString() + ":");
configureWrapper.accept(cmd);
cmd.executeAndAssertHelloAppImageCreated();
if (isAppImageValid(cmd)) {
verifyAppImage(cmd);
}
} else {
new PackageTest()
.forTypes(packageTypes)
.configureHelloApp(JAR_PATH.toString() + ":")
.addInitializer(configureWrapper)
.addInstallVerifier(InOutPathTest::verifyAppImage)
.run(CREATE_AND_UNPACK);
}
}
private static void outputDirInInputDir(JPackageCommand cmd) throws
IOException {
// Set output dir as a subdir of input dir
Path outputDir = cmd.inputDir().resolve("out");
TKit.createDirectories(outputDir);
cmd.setArgumentValue("--dest", outputDir);
}
private static void outputDirSameAsInputDir(JPackageCommand cmd) throws
IOException {
// Set output dir the same as the input dir
cmd.setArgumentValue("--dest", cmd.inputDir());
}
private static void tempDirInInputDir(JPackageCommand cmd) {
// Set temp dir as a subdir of input dir
Path tmpDir = cmd.inputDir().resolve("tmp");
cmd.setArgumentValue("--temp", tmpDir);
}
private static void additionalContent(JPackageCommand cmd,
String argName, Path base) throws IOException {
Path appContentFile = base.resolve(base.toString().replaceAll("[\\\\/]",
"-") + "-foo.txt");
TKit.createDirectories(appContentFile.getParent());
TKit.createTextFile(appContentFile, List.of("Hello Duke!"));
cmd.addArguments(argName, appContentFile.getParent());
}
private static void verifyAppImage(JPackageCommand cmd) throws IOException {
if (!isAppImageValid(cmd)) {
// Don't verify the contents of app image as it is invalid.
// jpackage exited without getting stuck in infinite spiral.
// No more expectations from the tool for the give arguments.
return;
}
final Path rootDir = cmd.isImagePackageType() ? cmd.outputBundle() : cmd.pathToUnpackedPackageFile(
cmd.appInstallationDirectory());
final Path appDir = ApplicationLayout.platformAppImage().resolveAt(
rootDir).appDirectory();
final var knownFiles = Set.of(
JAR_PATH.getName(0).toString(),
PackageFile.getPathInAppImage(Path.of("")).getFileName().toString(),
AppImageFile.getPathInAppImage(Path.of("")).getFileName().toString(),
cmd.name() + ".cfg"
);
TKit.assertFileExists(appDir.resolve(JAR_PATH));
try (Stream<Path> actualFilesStream = Files.list(appDir)) {
var unexpectedFiles = actualFilesStream.map(path -> {
return path.getFileName().toString();
}).filter(Predicate.not(knownFiles::contains)).toList();
TKit.assertStringListEquals(List.of(), unexpectedFiles,
"Check there are no unexpected files in `app` folder");
}
}
private static final record Envelope(ThrowingConsumer<JPackageCommand> value, String label) {
@Override
public String toString() {
// Will produce the same test description for the same label every
// time it's executed.
// The test runner will keep the same test output directory.
return label;
}
}
private final Set<PackageType> packageTypes;
private final ThrowingConsumer<JPackageCommand> configure;
// Placing jar file in the "Resources" subdir of the input directory would allow
// to use the input directory with `--app-content` on OSX.
// For other platforms it doesn't matter. Keep it the same across
// all platforms for simplicity.
private static final Path JAR_PATH = Path.of("Resources/duke.jar");
private static final String ALL_NATIVE_PACKAGE_TYPES = "NATIVE";
}

View File

@ -28,6 +28,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.ArrayList;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Pattern;
@ -41,6 +42,8 @@ import jdk.jpackage.test.Executor;
import jdk.jpackage.test.JavaTool;
import jdk.jpackage.test.Annotations.Test;
import jdk.jpackage.test.Annotations.Parameter;
import jdk.jpackage.test.Functional.ThrowingConsumer;
import static jdk.jpackage.test.RunnablePackageTest.Action.CREATE_AND_UNPACK;
import static jdk.jpackage.test.WindowsHelper.getTempDirectory;
@ -249,6 +252,63 @@ public final class BasicTest {
.executeAndAssertHelloAppImageCreated();
}
@Test
@Parameter("true")
@Parameter("false")
public void testNoOutputDir(boolean appImage) throws Throwable {
var cmd = JPackageCommand.helloAppImage();
final var execDir = cmd.outputDir();
final ThrowingConsumer<JPackageCommand> initializer = cmdNoOutputDir -> {
cmd.executePrerequisiteActions();
final var pkgType = cmdNoOutputDir.packageType();
cmdNoOutputDir
.clearArguments()
.addArguments(cmd.getAllArguments())
// Restore the value of `--type` parameter.
.setPackageType(pkgType)
.removeArgumentWithValue("--dest")
.setArgumentValue("--input", execDir.relativize(cmd.inputDir()))
.setDirectory(execDir)
// Force to use jpackage as executable because we need to
// change the current directory.
.useToolProvider(false);
Optional.ofNullable(cmdNoOutputDir.getArgumentValue("--runtime-image",
() -> null, Path::of)).ifPresent(runtimePath -> {
if (!runtimePath.isAbsolute()) {
cmdNoOutputDir.setArgumentValue("--runtime-image",
execDir.relativize(runtimePath));
}
});
// JPackageCommand.execute() will not do the cleanup if `--dest` parameter
// is not specified, do it manually.
TKit.createDirectories(execDir);
TKit.deleteDirectoryContentsRecursive(execDir);
};
if (appImage) {
var cmdNoOutputDir = new JPackageCommand()
.setPackageType(cmd.packageType());
initializer.accept(cmdNoOutputDir);
cmdNoOutputDir.executeAndAssertHelloAppImageCreated();
} else {
// Save time by packing non-functional runtime.
// Build the runtime in app image only. This is sufficient coverage.
cmd.setFakeRuntime();
new PackageTest()
.addInitializer(initializer)
.addInstallVerifier(HelloApp::executeLauncherAndVerifyOutput)
// Prevent adding `--dest` parameter to jpackage command line.
.ignoreBundleOutputDir()
.run(CREATE_AND_UNPACK);
}
}
@Test
@Parameter("ALL-MODULE-PATH")
@Parameter("ALL-DEFAULT")