8350013: Add a test for JDK-8150442

Reviewed-by: almatvee
This commit is contained in:
Alexey Semenyuk 2025-03-04 20:30:52 +00:00
parent a21302bb32
commit 3e86b3a879
8 changed files with 1719 additions and 481 deletions

View File

@ -0,0 +1,894 @@
/*
* Copyright (c) 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
* 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.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import jdk.jpackage.internal.util.function.ThrowingBiConsumer;
import jdk.jpackage.internal.util.function.ThrowingConsumer;
import jdk.jpackage.internal.util.function.ThrowingRunnable;
import jdk.jpackage.test.Annotations.Parameter;
import jdk.jpackage.test.Annotations.ParameterSupplier;
import jdk.jpackage.test.Annotations.Test;
import jdk.jpackage.test.PackageTest.PackageHandlers;
import jdk.jpackage.test.RunnablePackageTest.Action;
public class PackageTestTest extends JUnitAdapter {
private interface Verifiable {
void verify();
}
private static class CallbackFactory {
CallbackFactory(int tickCount) {
this.tickCount = tickCount;
}
CountingInstaller createInstaller(int exitCode) {
return new CountingInstaller(tickCount, exitCode);
}
CountingConsumer createUninstaller() {
return new CountingConsumer(tickCount, "uninstall");
}
CountingUnpacker createUnpacker() {
return new CountingUnpacker(tickCount);
}
CountingConsumer createInitializer() {
return new CountingConsumer(tickCount, "init");
}
CountingRunnable createRunOnceInitializer() {
return new CountingRunnable(tickCount, "once-init");
}
CountingConsumer createInstallVerifier() {
return new CountingConsumer(tickCount, "on-install");
}
CountingConsumer createUninstallVerifier() {
return new CountingConsumer(tickCount, "on-uninstall");
}
CountingConsumer createBundleVerifier() {
return new CountingConsumer(tickCount, "on-bundle");
}
CountingBundleVerifier createBundleVerifier(int jpackageExitCode) {
return new CountingBundleVerifier(tickCount, jpackageExitCode);
}
private final int tickCount;
}
private final static int ERROR_EXIT_CODE_JPACKAGE = 35;
private final static int ERROR_EXIT_CODE_INSTALL = 27;
private final static CallbackFactory NEVER = new CallbackFactory(0);
private final static CallbackFactory ONCE = new CallbackFactory(1);
private final static CallbackFactory TWICE = new CallbackFactory(2);
enum BundleVerifier {
ONCE_SUCCESS(ONCE),
ONCE_FAIL(ONCE),
NEVER(PackageTestTest.NEVER),
ONCE_SUCCESS_EXIT_CODE(ONCE, 0),
ONCE_FAIL_EXIT_CODE(ONCE, ERROR_EXIT_CODE_JPACKAGE),
NEVER_EXIT_CODE(PackageTestTest.NEVER, 0);
BundleVerifier(CallbackFactory factory) {
specSupplier = () -> new BundleVerifierSpec(Optional.of(factory.createBundleVerifier()), Optional.empty());
}
BundleVerifier(CallbackFactory factory, int jpackageExitCode) {
specSupplier = () -> new BundleVerifierSpec(Optional.empty(),
Optional.of(factory.createBundleVerifier(jpackageExitCode)));
}
BundleVerifierSpec spec() {
return specSupplier.get();
}
private final Supplier<BundleVerifierSpec> specSupplier;
}
private static class TickCounter implements Verifiable {
TickCounter(int expectedTicks) {
this.expectedTicks = expectedTicks;
}
void tick() {
ticks++;
}
@Override
public void verify() {
switch (expectedTicks) {
case 0 -> {
TKit.assertEquals(expectedTicks, ticks, String.format("%s: never called", this));
}
case 1 -> {
TKit.assertEquals(expectedTicks, ticks, String.format("%s: called once", this));
}
case 2 -> {
TKit.assertEquals(expectedTicks, ticks, String.format("%s: called twice", this));
}
default -> {
TKit.assertEquals(expectedTicks, ticks, toString());
}
}
}
protected int tickCount() {
return ticks;
}
protected static String getDescription(TickCounter o) {
return "tk=" + o.expectedTicks;
}
private int ticks;
protected final int expectedTicks;
}
private static class CountingConsumer extends TickCounter implements ThrowingConsumer<JPackageCommand> {
@Override
public void accept(JPackageCommand cmd) {
tick();
}
@Override
public String toString() {
return String.format("%s(%s)", label, TickCounter.getDescription(this));
}
CountingConsumer(int expectedTicks, String label) {
super(expectedTicks);
this.label = Objects.requireNonNull(label);
}
private final String label;
}
private static class CountingRunnable extends TickCounter implements ThrowingRunnable {
@Override
public void run() {
tick();
}
@Override
public String toString() {
return String.format("%s(%s)", label, TickCounter.getDescription(this));
}
CountingRunnable(int expectedTicks, String label) {
super(expectedTicks);
this.label = Objects.requireNonNull(label);
}
private final String label;
}
private static class CountingBundleVerifier extends TickCounter implements ThrowingBiConsumer<JPackageCommand, Executor.Result> {
@Override
public void accept(JPackageCommand cmd, Executor.Result result) {
tick();
jpackageExitCode = result.exitCode();
}
@Override
public void verify() {
super.verify();
if (expectedTicks > 0) {
TKit.assertEquals(expectedJPackageExitCode, jpackageExitCode, String.format("%s: run jpackage", this));
}
}
@Override
public String toString() {
return String.format("on-bundle-ex(exit=%d, %s)", expectedJPackageExitCode, TickCounter.getDescription(this));
}
CountingBundleVerifier(int expectedTicks, int expectedJPackageExitCode) {
super(expectedTicks);
this.expectedJPackageExitCode = expectedJPackageExitCode;
}
private int jpackageExitCode;
private final int expectedJPackageExitCode;
}
private final static class CountingInstaller extends TickCounter implements Function<JPackageCommand, Integer> {
@Override
public Integer apply(JPackageCommand cmd) {
tick();
return exitCode;
}
@Override
public String toString() {
return String.format("install(exit=%d, %s)", exitCode, TickCounter.getDescription(this));
}
CountingInstaller(int expectedTicks, int exitCode) {
super(expectedTicks);
this.exitCode = exitCode;
}
private final int exitCode;
}
private static class CountingUnpacker extends TickCounter implements BiFunction<JPackageCommand, Path, Path> {
@Override
public Path apply(JPackageCommand cmd, Path path) {
tick();
try {
Files.createDirectories(path.resolve("mockup-installdir"));
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
unpackPaths.add(path);
return path;
}
@Override
public String toString() {
return String.format("unpack(%s)", TickCounter.getDescription(this));
}
CountingUnpacker(int expectedTicks) {
super(expectedTicks);
}
List<Path> unpackPaths() {
return unpackPaths;
}
private final List<Path> unpackPaths = new ArrayList<>();
}
record BundleVerifierSpec(Optional<CountingConsumer> verifier, Optional<CountingBundleVerifier> verifierWithExitCode) {
BundleVerifierSpec {
if (verifier.isPresent() == verifierWithExitCode.isPresent()) {
throw new IllegalArgumentException();
}
}
Verifiable apply(PackageTest test) {
verifier.ifPresent(test::addBundleVerifier);
verifierWithExitCode.ifPresent(test::addBundleVerifier);
return verifier.map(Verifiable.class::cast).orElseGet(verifierWithExitCode::orElseThrow);
}
@Override
public String toString() {
return verifier.map(Verifiable.class::cast).orElseGet(verifierWithExitCode::orElseThrow).toString();
}
}
record PackageHandlersSpec(CountingInstaller installer, CountingConsumer uninstaller,
Optional<CountingUnpacker> unpacker, int installExitCode) {
PackageHandlers createPackageHandlers(Consumer<Verifiable> verifiableAccumulator) {
List.of(installer, uninstaller).forEach(verifiableAccumulator::accept);
unpacker.ifPresent(verifiableAccumulator::accept);
return new PackageHandlers(installer, uninstaller::accept, unpacker);
}
}
record TestSpec(PackageType type, PackageHandlersSpec handlersSpec,
List<CountingConsumer> initializers, List<BundleVerifierSpec> bundleVerifierSpecs,
List<CountingConsumer> installVerifiers, List<CountingConsumer> uninstallVerifiers,
int expectedJPackageExitCode, int actualJPackageExitCode, List<Action> actions) {
PackageTest createTest(Consumer<Verifiable> verifiableAccumulator) {
return createTest(handlersSpec.createPackageHandlers(verifiableAccumulator));
}
PackageTest createTest(PackageHandlers handlers) {
return new PackageTest().jpackageFactory(() -> {
return new JPackageCommand() {
@Override
public Path outputBundle() {
return outputDir().resolve("mockup-bundle" + super.packageType().getSuffix());
}
@Override
public PackageType packageType() {
return null;
}
@Override
JPackageCommand assertAppLayout() {
return this;
}
@Override
JPackageCommand createImmutableCopy() {
return this;
}
@Override
public void verifyIsOfType(PackageType ... types) {
}
@Override
public String getPrintableCommandLine() {
return "'mockup jpackage'";
}
@Override
public Executor.Result execute(int expectedExitCode) {
final var outputBundle = outputBundle();
try {
Files.createDirectories(outputBundle.getParent());
if (actualJPackageExitCode == 0) {
Files.createFile(outputBundle);
}
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
return new Executor.Result(actualJPackageExitCode, null,
this::getPrintableCommandLine).assertExitCodeIs(expectedExitCode);
}
};
}).setExpectedExitCode(expectedJPackageExitCode)
.setExpectedInstallExitCode(handlersSpec.installExitCode)
.isPackageTypeSupported(type -> true)
.forTypes().packageHandlers(handlers);
}
void configureInitializers(PackageTest test, Consumer<Verifiable> verifiableAccumulator) {
for (final var initializer : initializers) {
verifiableAccumulator.accept(initializer);
test.addInitializer(initializer);
}
}
void configureBundleVerifiers(PackageTest test, Consumer<Verifiable> verifiableAccumulator) {
for (final var verifierSpec : bundleVerifierSpecs) {
verifiableAccumulator.accept(verifierSpec.apply(test));
}
}
void configureInstallVerifiers(PackageTest test, Consumer<Verifiable> verifiableAccumulator) {
for (final var verifier : installVerifiers) {
verifiableAccumulator.accept(verifier);
test.addInstallVerifier(verifier);
}
}
void configureUninstallVerifiers(PackageTest test, Consumer<Verifiable> verifiableAccumulator) {
for (final var verifier : uninstallVerifiers) {
verifiableAccumulator.accept(verifier);
test.addUninstallVerifier(verifier);
}
}
void run(PackageTest test) {
final boolean expectedSuccess = (expectedJPackageExitCode == actualJPackageExitCode);
TKit.assertAssert(expectedSuccess, () -> {
test.run(actions.toArray(Action[]::new));
});
}
List<Verifiable> run() {
return run(Optional.empty());
}
List<Verifiable> run(Consumer<PackageTest> customConfigure) {
return run(Optional.of(customConfigure));
}
private List<Verifiable> run(Optional<Consumer<PackageTest>> customConfigure) {
final List<Verifiable> verifiers = new ArrayList<>();
final var test = createTest(verifiers::add);
test.forTypes(type);
configureInitializers(test, verifiers::add);
configureBundleVerifiers(test, verifiers::add);
configureInstallVerifiers(test, verifiers::add);
configureUninstallVerifiers(test, verifiers::add);
customConfigure.ifPresent(callback -> callback.accept(test));
run(test);
verifiers.forEach(Verifiable::verify);
return verifiers;
}
}
private final static class TestSpecBuilder {
TestSpecBuilder type(PackageType v) {
type = Objects.requireNonNull(v);
return this;
}
TestSpecBuilder install(CallbackFactory v) {
install = Objects.requireNonNull(v);
return this;
}
TestSpecBuilder uninstall(CallbackFactory v) {
uninstall = Objects.requireNonNull(v);
return this;
}
TestSpecBuilder unpack(CallbackFactory v) {
unpack = v;
return this;
}
TestSpecBuilder installExitCode(int v) {
installExitCode = v;
return this;
}
TestSpecBuilder jpackageExitCode(int v) {
return expectedJPackageExitCode(v).actualJPackageExitCode(v);
}
TestSpecBuilder expectedJPackageExitCode(int v) {
expectedJPackageExitCode = v;
return this;
}
TestSpecBuilder actualJPackageExitCode(int v) {
actualJPackageExitCode = v;
return this;
}
TestSpecBuilder addActions(Action... v) {
actions.addAll(List.of(v));
return this;
}
TestSpecBuilder actions(Action... v) {
actions.clear();
return addActions(v);
}
TestSpecBuilder doCreateAndUnpack() {
actions(Action.CREATE_AND_UNPACK);
install(NEVER);
uninstall(NEVER);
if (willHaveBundle()) {
overrideNonNullUnpack(ONCE);
} else {
overrideNonNullUnpack(NEVER);
}
initializers(ONCE);
if (expectedJPackageExitCode != actualJPackageExitCode) {
bundleVerifiers(BundleVerifier.NEVER.spec());
} else if (expectedJPackageExitCode == 0) {
bundleVerifiers(BundleVerifier.ONCE_SUCCESS.spec());
bundleVerifiers(BundleVerifier.ONCE_SUCCESS_EXIT_CODE.spec());
} else {
bundleVerifiers(BundleVerifier.ONCE_FAIL.spec());
if (expectedJPackageExitCode == ERROR_EXIT_CODE_JPACKAGE) {
bundleVerifiers(BundleVerifier.ONCE_FAIL_EXIT_CODE.spec());
}
}
uninstallVerifiers(NEVER);
if (willVerifyUnpack()) {
installVerifiers(ONCE);
} else {
installVerifiers(NEVER);
}
return this;
}
TestSpecBuilder doCreateUnpackInstallUninstall() {
actions(Action.CREATE, Action.UNPACK, Action.VERIFY_INSTALL, Action.INSTALL,
Action.VERIFY_INSTALL, Action.UNINSTALL, Action.VERIFY_UNINSTALL);
initializers(ONCE);
uninstallVerifiers(NEVER);
if (willHaveBundle()) {
overrideNonNullUnpack(ONCE);
install(ONCE);
if (installExitCode == 0) {
uninstall(ONCE);
uninstallVerifiers(ONCE);
} else {
uninstall(NEVER);
}
} else {
overrideNonNullUnpack(NEVER);
install(NEVER);
uninstall(NEVER);
}
if (expectedJPackageExitCode != actualJPackageExitCode) {
bundleVerifiers(BundleVerifier.NEVER.spec());
installVerifiers(NEVER);
} else if (expectedJPackageExitCode == 0) {
bundleVerifiers(BundleVerifier.ONCE_SUCCESS.spec());
bundleVerifiers(BundleVerifier.ONCE_SUCCESS_EXIT_CODE.spec());
if (installExitCode == 0) {
if (willVerifyUnpack()) {
installVerifiers(TWICE);
} else {
installVerifiers(ONCE);
}
} else {
if (willVerifyUnpack()) {
installVerifiers(ONCE);
} else {
installVerifiers(NEVER);
}
}
} else {
bundleVerifiers(BundleVerifier.ONCE_FAIL.spec());
if (expectedJPackageExitCode == ERROR_EXIT_CODE_JPACKAGE) {
bundleVerifiers(BundleVerifier.ONCE_FAIL_EXIT_CODE.spec());
}
installVerifiers(NEVER);
}
return this;
}
TestSpecBuilder addInitializers(CallbackFactory... v) {
initializers.addAll(List.of(v));
return this;
}
TestSpecBuilder addBundleVerifiers(BundleVerifierSpec... v) {
bundleVerifiers.addAll(List.of(v));
return this;
}
TestSpecBuilder addInstallVerifiers(CallbackFactory... v) {
installVerifiers.addAll(List.of(v));
return this;
}
TestSpecBuilder addUninstallVerifiers(CallbackFactory... v) {
uninstallVerifiers.addAll(List.of(v));
return this;
}
TestSpecBuilder initializers(CallbackFactory... v) {
initializers.clear();
return addInitializers(v);
}
TestSpecBuilder bundleVerifiers(BundleVerifierSpec... v) {
bundleVerifiers.clear();
return addBundleVerifiers(v);
}
TestSpecBuilder installVerifiers(CallbackFactory... v) {
installVerifiers.clear();
return addInstallVerifiers(v);
}
TestSpecBuilder uninstallVerifiers(CallbackFactory... v) {
uninstallVerifiers.clear();
return addUninstallVerifiers(v);
}
TestSpec create() {
final var handlersSpec = new PackageHandlersSpec(
install.createInstaller(installExitCode), uninstall.createUninstaller(),
Optional.ofNullable(unpack).map(CallbackFactory::createUnpacker), installExitCode);
return new TestSpec(type, handlersSpec,
initializers.stream().map(CallbackFactory::createInitializer).toList(),
bundleVerifiers,
installVerifiers.stream().map(CallbackFactory::createInstallVerifier).toList(),
uninstallVerifiers.stream().map(CallbackFactory::createUninstallVerifier).toList(),
expectedJPackageExitCode,
actualJPackageExitCode, actions);
}
boolean willVerifyCreate() {
return actions.contains(Action.CREATE) && actualJPackageExitCode == 0 && expectedJPackageExitCode == actualJPackageExitCode;
}
boolean willHaveBundle() {
return !actions.contains(Action.CREATE) || willVerifyCreate();
}
boolean willVerifyUnpack() {
return actions.contains(Action.UNPACK) && willHaveBundle() && unpack != null;
}
boolean willVerifyInstall() {
return (actions.contains(Action.INSTALL) && installExitCode == 0) && willHaveBundle();
}
private void overrideNonNullUnpack(CallbackFactory v) {
if (unpack != null) {
unpack(v);
}
}
private PackageType type = PackageType.LINUX_RPM;
private CallbackFactory install = NEVER;
private CallbackFactory uninstall = NEVER;
private CallbackFactory unpack = NEVER;
private int installExitCode;
private final List<CallbackFactory> initializers = new ArrayList<>();
private final List<BundleVerifierSpec> bundleVerifiers = new ArrayList<>();
private final List<CallbackFactory> installVerifiers = new ArrayList<>();
private final List<CallbackFactory> uninstallVerifiers = new ArrayList<>();
private int expectedJPackageExitCode;
private int actualJPackageExitCode;
private final List<Action> actions = new ArrayList<>();
}
@Test
@ParameterSupplier
public void test(TestSpec spec) {
spec.run();
}
public static List<Object[]> test() {
List<TestSpec> data = new ArrayList<>();
for (boolean withUnpack : List.of(false, true)) {
for (int actualJPackageExitCode : List.of(0, 1, ERROR_EXIT_CODE_INSTALL)) {
for (int expectedJPackageExitCode : List.of(0, 1, ERROR_EXIT_CODE_INSTALL)) {
data.add(new TestSpecBuilder()
.unpack(withUnpack ? ONCE : null)
.actualJPackageExitCode(actualJPackageExitCode)
.expectedJPackageExitCode(expectedJPackageExitCode)
.doCreateAndUnpack().create());
}
}
}
for (boolean withUnpack : List.of(false, true)) {
for (int installExitCode : List.of(0, 1, ERROR_EXIT_CODE_INSTALL)) {
for (int actualJPackageExitCode : List.of(0, 1, ERROR_EXIT_CODE_JPACKAGE)) {
for (int expectedJPackageExitCode : List.of(0, 1, ERROR_EXIT_CODE_JPACKAGE)) {
data.add(new TestSpecBuilder()
.unpack(withUnpack ? ONCE : null)
.installExitCode(installExitCode)
.actualJPackageExitCode(actualJPackageExitCode)
.expectedJPackageExitCode(expectedJPackageExitCode)
.doCreateUnpackInstallUninstall().create());
}
}
}
}
data.add(new TestSpecBuilder()
.actions(Action.VERIFY_INSTALL, Action.UNINSTALL, Action.VERIFY_INSTALL, Action.VERIFY_UNINSTALL)
.uninstall(ONCE)
.initializers(ONCE)
.bundleVerifiers(BundleVerifier.NEVER.spec())
.installVerifiers(TWICE)
.uninstallVerifiers(ONCE)
.create());
return data.stream().map(v -> {
return new Object[] {v};
}).toList();
}
@Test
@ParameterSupplier
public void testDisableInstallerUninstaller(TestSpec spec, boolean disableInstaller, boolean disableUninstaller) {
spec.run(test -> {
if (disableInstaller) {
test.disablePackageInstaller();
}
if (disableUninstaller) {
test.disablePackageUninstaller();
}
});
}
public static List<Object[]> testDisableInstallerUninstaller() {
List<Object[]> data = new ArrayList<>();
for (boolean disableInstaller : List.of(true, false)) {
for (boolean disableUninstaller : List.of(true, false)) {
if (disableInstaller || disableUninstaller) {
final var builder = new TestSpecBuilder().doCreateUnpackInstallUninstall();
if (disableInstaller) {
builder.install(NEVER);
}
if (disableUninstaller) {
builder.uninstall(NEVER);
}
data.add(new Object[] { builder.create(), disableInstaller, disableUninstaller });
}
}
}
return data;
}
private static List<Path> getUnpackPaths(Collection<Verifiable> verifiers) {
return verifiers.stream()
.filter(CountingUnpacker.class::isInstance)
.map(CountingUnpacker.class::cast)
.map(CountingUnpacker::unpackPaths)
.reduce((x , y) -> {
throw new UnsupportedOperationException();
}).orElseThrow();
}
@Test
public void testUnpackTwice() {
final var testSpec = new TestSpecBuilder()
.actions(Action.CREATE, Action.UNPACK, Action.VERIFY_INSTALL, Action.UNPACK, Action.VERIFY_INSTALL)
.unpack(TWICE)
.initializers(ONCE)
.installVerifiers(TWICE)
.create();
final var unpackPaths = getUnpackPaths(testSpec.run());
TKit.assertEquals(2, unpackPaths.size(), "Check the bundle was unpacked in different directories");
unpackPaths.forEach(dir -> {
TKit.assertTrue(dir.startsWith(TKit.workDir()), "Check unpack directory is inside of the test work directory");
});
}
@Test
public void testDeleteUnpackDirs() {
final int unpackActionCount = 4;
final var testSpec = new TestSpecBuilder()
.actions(Action.UNPACK, Action.UNPACK, Action.UNPACK, Action.UNPACK)
.unpack(new CallbackFactory(unpackActionCount) {
@Override
CountingUnpacker createUnpacker() {
return new CountingUnpacker(unpackActionCount) {
@Override
public Path apply(JPackageCommand cmd, Path path) {
switch (tickCount()) {
case 0 -> {
}
case 2 -> {
path = path.resolve("foo");
}
case 1, 3 -> {
try {
path = Files.createTempDirectory("jpackage-test");
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
default -> {
throw new IllegalStateException();
}
}
return super.apply(cmd, path);
}
};
}
})
.initializers(ONCE)
.create();
final var unpackPaths = getUnpackPaths(testSpec.run());
TKit.assertEquals(unpackActionCount, unpackPaths.size(), "Check the bundle was unpacked in different directories");
// Unpack directories within the test work directory must exist.
TKit.assertDirectoryExists(unpackPaths.get(0));
TKit.assertDirectoryExists(unpackPaths.get(2));
// Unpack directories outside of the test work directory must be deleted.
TKit.assertPathExists(unpackPaths.get(1), false);
TKit.assertPathExists(unpackPaths.get(3), false);
}
@Test
public void testRunOnceInitializer() {
final var testSpec = new TestSpecBuilder().doCreateAndUnpack().unpack(TWICE).create();
final var initializer = TWICE.createInitializer();
final var runOnceInitializer = ONCE.createRunOnceInitializer();
testSpec.run(test -> {
test.forTypes(PackageType.LINUX_RPM, PackageType.WIN_MSI)
.addRunOnceInitializer(runOnceInitializer)
.addInitializer(initializer);
});
initializer.verify();
runOnceInitializer.verify();
}
@Test
@Parameter("0")
@Parameter("1")
public void testPurge(int jpackageExitCode) {
Path[] outputBundle = new Path[1];
final var builder = new TestSpecBuilder();
builder.actions(Action.CREATE).initializers(new CallbackFactory(1) {
@Override
CountingConsumer createInitializer() {
return new CountingConsumer(1, "custom-init") {
@Override
public void accept(JPackageCommand cmd) {
outputBundle[0] = cmd.outputBundle();
super.accept(cmd);
}
};
}
}).create().run();
TKit.assertFileExists(outputBundle[0]);
builder.actions(Action.PURGE).initializers(ONCE).jpackageExitCode(jpackageExitCode).create().run();
TKit.assertPathExists(outputBundle[0], false);
}
@Test
public void testPackageTestOrder() {
Set<PackageType> packageTypes = new LinkedHashSet<>();
final var initializer = new CountingConsumer(PackageType.NATIVE.size(), "custom-init") {
@Override
public void accept(JPackageCommand cmd) {
packageTypes.add(new JPackageCommand().setArgumentValue(
"--type", cmd.getArgumentValue("--type")).packageType());
super.accept(cmd);
}
};
new TestSpecBuilder().actions(Action.CREATE).create().run(test -> {
test.forTypes().addInitializer(initializer);
});
initializer.verify();
final var expectedOrder = PackageType.NATIVE.stream()
.sorted().map(PackageType::name).toList();
final var actualOrder = packageTypes.stream().map(PackageType::name).toList();
TKit.assertStringListEquals(expectedOrder, actualOrder, "Check the order or packaging");
}
}

View File

@ -25,6 +25,7 @@ package jdk.jpackage.test;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.SecureRandom;
@ -77,6 +78,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
appLayoutAsserts = cmd.appLayoutAsserts;
outputValidator = cmd.outputValidator;
executeInDirectory = cmd.executeInDirectory;
winMsiLogFile = cmd.winMsiLogFile;
}
JPackageCommand createImmutableCopy() {
@ -998,6 +1000,22 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
return this;
}
JPackageCommand winMsiLogFile(Path v) {
this.winMsiLogFile = v;
return this;
}
public Optional<Path> winMsiLogFile() {
return Optional.ofNullable(winMsiLogFile);
}
public Optional<Stream<String>> winMsiLogFileContents() {
return winMsiLogFile().map(ThrowingFunction.toFunction(msiLog -> {
// MSI log files are UTF16LE-encoded
return Files.lines(msiLog, StandardCharsets.UTF_16LE);
}));
}
private JPackageCommand adjustArgumentsBeforeExecution() {
if (!isWithToolProvider()) {
// if jpackage is launched as a process then set the jlink.debug system property
@ -1175,6 +1193,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
private final Actions prerequisiteActions;
private final Actions verifyActions;
private Path executeInDirectory;
private Path winMsiLogFile;
private Set<AppLayoutAssert> appLayoutAsserts = Set.of(AppLayoutAssert.values());
private Consumer<Stream<String>> outputValidator;
private static boolean defaultWithToolProvider;

View File

@ -195,58 +195,61 @@ public final class LinuxHelper {
}
static PackageHandlers createDebPackageHandlers() {
PackageHandlers deb = new PackageHandlers();
deb.installHandler = cmd -> {
cmd.verifyIsOfType(PackageType.LINUX_DEB);
Executor.of("sudo", "dpkg", "-i")
.addArgument(cmd.outputBundle())
.execute();
};
deb.uninstallHandler = cmd -> {
cmd.verifyIsOfType(PackageType.LINUX_DEB);
var packageName = getPackageName(cmd);
String script = String.format("! dpkg -s %s || sudo dpkg -r %s",
packageName, packageName);
Executor.of("sh", "-c", script).execute();
};
deb.unpackHandler = (cmd, destinationDir) -> {
cmd.verifyIsOfType(PackageType.LINUX_DEB);
Executor.of("dpkg", "-x")
.addArgument(cmd.outputBundle())
.addArgument(destinationDir)
.execute();
return destinationDir;
};
return deb;
return new PackageHandlers(LinuxHelper::installDeb, LinuxHelper::uninstallDeb, LinuxHelper::unpackDeb);
}
private static int installDeb(JPackageCommand cmd) {
cmd.verifyIsOfType(PackageType.LINUX_DEB);
return Executor.of("sudo", "dpkg", "-i")
.addArgument(cmd.outputBundle())
.execute().getExitCode();
}
private static void uninstallDeb(JPackageCommand cmd) {
cmd.verifyIsOfType(PackageType.LINUX_DEB);
var packageName = getPackageName(cmd);
String script = String.format("! dpkg -s %s || sudo dpkg -r %s",
packageName, packageName);
Executor.of("sh", "-c", script).execute();
}
private static Path unpackDeb(JPackageCommand cmd, Path destinationDir) {
cmd.verifyIsOfType(PackageType.LINUX_DEB);
Executor.of("dpkg", "-x")
.addArgument(cmd.outputBundle())
.addArgument(destinationDir)
.execute(0);
return destinationDir;
}
static PackageHandlers createRpmPackageHandlers() {
PackageHandlers rpm = new PackageHandlers();
rpm.installHandler = cmd -> {
cmd.verifyIsOfType(PackageType.LINUX_RPM);
Executor.of("sudo", "rpm", "-U")
.addArgument(cmd.outputBundle())
.execute();
};
rpm.uninstallHandler = cmd -> {
cmd.verifyIsOfType(PackageType.LINUX_RPM);
var packageName = getPackageName(cmd);
String script = String.format("! rpm -q %s || sudo rpm -e %s",
packageName, packageName);
Executor.of("sh", "-c", script).execute();
};
rpm.unpackHandler = (cmd, destinationDir) -> {
cmd.verifyIsOfType(PackageType.LINUX_RPM);
Executor.of("sh", "-c", String.format(
"rpm2cpio '%s' | cpio -idm --quiet",
JPackageCommand.escapeAndJoin(
cmd.outputBundle().toAbsolutePath().toString())))
.setDirectory(destinationDir)
.execute();
return destinationDir;
};
return new PackageHandlers(LinuxHelper::installRpm, LinuxHelper::uninstallRpm, LinuxHelper::unpackRpm);
}
return rpm;
private static int installRpm(JPackageCommand cmd) {
cmd.verifyIsOfType(PackageType.LINUX_RPM);
return Executor.of("sudo", "rpm", "-U")
.addArgument(cmd.outputBundle())
.execute().getExitCode();
}
private static void uninstallRpm(JPackageCommand cmd) {
cmd.verifyIsOfType(PackageType.LINUX_RPM);
var packageName = getPackageName(cmd);
String script = String.format("! rpm -q %s || sudo rpm -e %s",
packageName, packageName);
Executor.of("sh", "-c", script).execute();
}
private static Path unpackRpm(JPackageCommand cmd, Path destinationDir) {
cmd.verifyIsOfType(PackageType.LINUX_RPM);
Executor.of("sh", "-c", String.format(
"rpm2cpio '%s' | cpio -idm --quiet",
JPackageCommand.escapeAndJoin(
cmd.outputBundle().toAbsolutePath().toString())))
.setDirectory(destinationDir)
.execute(0);
return destinationDir;
}
static Path getLauncherPath(JPackageCommand cmd) {

View File

@ -150,113 +150,115 @@ public final class MacHelper {
}
static PackageHandlers createDmgPackageHandlers() {
PackageHandlers dmg = new PackageHandlers();
return new PackageHandlers(MacHelper::installDmg, MacHelper::uninstallDmg, MacHelper::unpackDmg);
}
dmg.installHandler = cmd -> {
withExplodedDmg(cmd, dmgImage -> {
Executor.of("sudo", "cp", "-r")
.addArgument(dmgImage)
.addArgument(getInstallationDirectory(cmd).getParent())
.execute();
});
};
dmg.unpackHandler = (cmd, destinationDir) -> {
Path unpackDir = destinationDir.resolve(
TKit.removeRootFromAbsolutePath(
getInstallationDirectory(cmd)).getParent());
try {
Files.createDirectories(unpackDir);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
private static int installDmg(JPackageCommand cmd) {
cmd.verifyIsOfType(PackageType.MAC_DMG);
withExplodedDmg(cmd, dmgImage -> {
Executor.of("sudo", "cp", "-r")
.addArgument(dmgImage)
.addArgument(getInstallationDirectory(cmd).getParent())
.execute(0);
});
return 0;
}
withExplodedDmg(cmd, dmgImage -> {
Executor.of("cp", "-r")
.addArgument(dmgImage)
.addArgument(unpackDir)
.execute();
});
return destinationDir;
};
dmg.uninstallHandler = cmd -> {
cmd.verifyIsOfType(PackageType.MAC_DMG);
Executor.of("sudo", "rm", "-rf")
.addArgument(cmd.appInstallationDirectory())
private static void uninstallDmg(JPackageCommand cmd) {
cmd.verifyIsOfType(PackageType.MAC_DMG);
Executor.of("sudo", "rm", "-rf")
.addArgument(cmd.appInstallationDirectory())
.execute();
}
private static Path unpackDmg(JPackageCommand cmd, Path destinationDir) {
cmd.verifyIsOfType(PackageType.MAC_DMG);
Path unpackDir = destinationDir.resolve(
TKit.removeRootFromAbsolutePath(
getInstallationDirectory(cmd)).getParent());
try {
Files.createDirectories(unpackDir);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
withExplodedDmg(cmd, dmgImage -> {
Executor.of("cp", "-r")
.addArgument(dmgImage)
.addArgument(unpackDir)
.execute();
};
return dmg;
});
return destinationDir;
}
static PackageHandlers createPkgPackageHandlers() {
PackageHandlers pkg = new PackageHandlers();
return new PackageHandlers(MacHelper::installPkg, MacHelper::uninstallPkg, MacHelper::unpackPkg);
}
pkg.installHandler = cmd -> {
cmd.verifyIsOfType(PackageType.MAC_PKG);
Executor.of("sudo", "/usr/sbin/installer", "-allowUntrusted", "-pkg")
.addArgument(cmd.outputBundle())
.addArguments("-target", "/")
private static int installPkg(JPackageCommand cmd) {
cmd.verifyIsOfType(PackageType.MAC_PKG);
return Executor.of("sudo", "/usr/sbin/installer", "-allowUntrusted", "-pkg")
.addArgument(cmd.outputBundle())
.addArguments("-target", "/")
.execute().getExitCode();
}
private static void uninstallPkg(JPackageCommand cmd) {
cmd.verifyIsOfType(PackageType.MAC_PKG);
if (Files.exists(getUninstallCommand(cmd))) {
Executor.of("sudo", "/bin/sh",
getUninstallCommand(cmd).toString()).execute();
} else {
Executor.of("sudo", "rm", "-rf")
.addArgument(cmd.appInstallationDirectory())
.execute();
};
pkg.unpackHandler = (cmd, destinationDir) -> {
cmd.verifyIsOfType(PackageType.MAC_PKG);
}
}
var dataDir = destinationDir.resolve("data");
private static Path unpackPkg(JPackageCommand cmd, Path destinationDir) {
cmd.verifyIsOfType(PackageType.MAC_PKG);
Executor.of("pkgutil", "--expand")
.addArgument(cmd.outputBundle())
.addArgument(dataDir) // We need non-existing folder
.execute();
var dataDir = destinationDir.resolve("data");
final Path unpackRoot = destinationDir.resolve("unpacked");
Executor.of("pkgutil", "--expand")
.addArgument(cmd.outputBundle())
.addArgument(dataDir) // We need non-existing folder
.execute();
// Unpack all ".pkg" files from $dataDir folder in $unpackDir folder
try (var dataListing = Files.list(dataDir)) {
dataListing.filter(file -> {
return ".pkg".equals(PathUtils.getSuffix(file.getFileName()));
}).forEach(ThrowingConsumer.toConsumer(pkgDir -> {
// Installation root of the package is stored in
// /pkg-info@install-location attribute in $pkgDir/PackageInfo xml file
var doc = createDocumentBuilder().parse(
new ByteArrayInputStream(Files.readAllBytes(
pkgDir.resolve("PackageInfo"))));
var xPath = XPathFactory.newInstance().newXPath();
final Path unpackRoot = destinationDir.resolve("unpacked");
final String installRoot = (String) xPath.evaluate(
"/pkg-info/@install-location", doc,
XPathConstants.STRING);
// Unpack all ".pkg" files from $dataDir folder in $unpackDir folder
try (var dataListing = Files.list(dataDir)) {
dataListing.filter(file -> {
return ".pkg".equals(PathUtils.getSuffix(file.getFileName()));
}).forEach(ThrowingConsumer.toConsumer(pkgDir -> {
// Installation root of the package is stored in
// /pkg-info@install-location attribute in $pkgDir/PackageInfo xml file
var doc = createDocumentBuilder().parse(
new ByteArrayInputStream(Files.readAllBytes(
pkgDir.resolve("PackageInfo"))));
var xPath = XPathFactory.newInstance().newXPath();
final Path unpackDir = unpackRoot.resolve(
TKit.removeRootFromAbsolutePath(Path.of(installRoot)));
final String installRoot = (String) xPath.evaluate(
"/pkg-info/@install-location", doc,
XPathConstants.STRING);
Files.createDirectories(unpackDir);
final Path unpackDir = unpackRoot.resolve(
TKit.removeRootFromAbsolutePath(Path.of(installRoot)));
Executor.of("tar", "-C")
.addArgument(unpackDir)
.addArgument("-xvf")
.addArgument(pkgDir.resolve("Payload"))
.execute();
}));
} catch (IOException ex) {
throw new RuntimeException(ex);
}
Files.createDirectories(unpackDir);
return unpackRoot;
};
pkg.uninstallHandler = cmd -> {
cmd.verifyIsOfType(PackageType.MAC_PKG);
if (Files.exists(getUninstallCommand(cmd))) {
Executor.of("sudo", "/bin/sh",
getUninstallCommand(cmd).toString()).execute();
} else {
Executor.of("sudo", "rm", "-rf")
.addArgument(cmd.appInstallationDirectory())
Executor.of("tar", "-C")
.addArgument(unpackDir)
.addArgument("-xvf")
.addArgument(pkgDir.resolve("Payload"))
.execute();
}
};
}));
} catch (IOException ex) {
throw new RuntimeException(ex);
}
return pkg;
return unpackRoot;
}
static void verifyBundleStructure(JPackageCommand cmd) {

View File

@ -22,6 +22,15 @@
*/
package jdk.jpackage.test;
import static jdk.jpackage.internal.util.function.ExceptionBox.rethrowUnchecked;
import static jdk.jpackage.internal.util.function.ThrowingBiConsumer.toBiConsumer;
import static jdk.jpackage.internal.util.function.ThrowingConsumer.toConsumer;
import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier;
import static jdk.jpackage.test.PackageType.LINUX;
import static jdk.jpackage.test.PackageType.MAC_PKG;
import static jdk.jpackage.test.PackageType.NATIVE;
import static jdk.jpackage.test.PackageType.WINDOWS;
import java.awt.GraphicsEnvironment;
import java.io.IOException;
import java.nio.file.Files;
@ -29,6 +38,7 @@ import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -40,27 +50,15 @@ import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import jdk.jpackage.internal.util.function.ThrowingBiConsumer;
import static jdk.jpackage.internal.util.function.ThrowingBiConsumer.toBiConsumer;
import jdk.jpackage.internal.util.function.ThrowingConsumer;
import static jdk.jpackage.internal.util.function.ThrowingConsumer.toConsumer;
import jdk.jpackage.internal.util.function.ThrowingRunnable;
import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier;
import static jdk.jpackage.internal.util.function.ExceptionBox.rethrowUnchecked;
import static jdk.jpackage.test.PackageType.LINUX;
import static jdk.jpackage.test.PackageType.LINUX_DEB;
import static jdk.jpackage.test.PackageType.LINUX_RPM;
import static jdk.jpackage.test.PackageType.MAC_DMG;
import static jdk.jpackage.test.PackageType.MAC_PKG;
import static jdk.jpackage.test.PackageType.NATIVE;
import static jdk.jpackage.test.PackageType.WINDOWS;
import static jdk.jpackage.test.PackageType.WIN_EXE;
import static jdk.jpackage.test.PackageType.WIN_MSI;
/**
@ -73,13 +71,18 @@ import static jdk.jpackage.test.PackageType.WIN_MSI;
public final class PackageTest extends RunnablePackageTest {
public PackageTest() {
isPackageTypeSupported = PackageType::isSupported;
jpackageFactory = JPackageCommand::new;
packageHandlers = new HashMap<>();
disabledInstallers = new HashSet<>();
disabledUninstallers = new HashSet<>();
excludeTypes = new HashSet<>();
forTypes();
setExpectedExitCode(0);
setExpectedInstallExitCode(0);
namedInitializers = new HashSet<>();
handlers = currentTypes.stream()
handlers = NATIVE.stream()
.collect(Collectors.toMap(v -> v, v -> new Handler()));
packageHandlers = createDefaultPackageHandlers();
}
public PackageTest excludeTypes(PackageType... types) {
@ -93,13 +96,13 @@ public final class PackageTest extends RunnablePackageTest {
public PackageTest forTypes(PackageType... types) {
Collection<PackageType> newTypes;
if (types == null || types.length == 0) {
if (types.length == 0) {
newTypes = NATIVE;
} else {
newTypes = Stream.of(types).collect(Collectors.toSet());
}
currentTypes = newTypes.stream()
.filter(PackageType::isSupported)
.filter(isPackageTypeSupported)
.filter(Predicate.not(excludeTypes::contains))
.collect(Collectors.toUnmodifiableSet());
return this;
@ -124,6 +127,11 @@ public final class PackageTest extends RunnablePackageTest {
return this;
}
public PackageTest setExpectedInstallExitCode(int v) {
expectedInstallExitCode = v;
return this;
}
public PackageTest ignoreBundleOutputDir() {
return ignoreBundleOutputDir(true);
}
@ -133,8 +141,8 @@ public final class PackageTest extends RunnablePackageTest {
return this;
}
private PackageTest addInitializer(ThrowingConsumer<JPackageCommand> v,
String id) {
private PackageTest addInitializer(ThrowingConsumer<JPackageCommand> v, String id) {
Objects.requireNonNull(v);
if (id != null) {
if (namedInitializers.contains(id)) {
return this;
@ -142,12 +150,12 @@ public final class PackageTest extends RunnablePackageTest {
namedInitializers.add(id);
}
currentTypes.forEach(type -> handlers.get(type).addInitializer(
toConsumer(v)));
currentTypes.forEach(type -> handlers.get(type).addInitializer(toConsumer(v)));
return this;
}
private PackageTest addRunOnceInitializer(ThrowingRunnable v, String id) {
Objects.requireNonNull(v);
return addInitializer(new ThrowingConsumer<JPackageCommand>() {
@Override
public void accept(JPackageCommand unused) throws Throwable {
@ -169,24 +177,26 @@ public final class PackageTest extends RunnablePackageTest {
return addRunOnceInitializer(v, null);
}
public PackageTest addBundleVerifier(
ThrowingBiConsumer<JPackageCommand, Executor.Result> v) {
currentTypes.forEach(type -> handlers.get(type).addBundleVerifier(
toBiConsumer(v)));
public PackageTest addBundleVerifier(ThrowingBiConsumer<JPackageCommand, Executor.Result> v) {
Objects.requireNonNull(v);
currentTypes.forEach(type -> handlers.get(type).addBundleVerifier(toBiConsumer(v)));
return this;
}
public PackageTest addBundleVerifier(ThrowingConsumer<JPackageCommand> v) {
Objects.requireNonNull(v);
return addBundleVerifier((cmd, unused) -> toConsumer(v).accept(cmd));
}
public PackageTest addBundlePropertyVerifier(String propertyName,
Predicate<String> pred, String predLabel) {
Objects.requireNonNull(propertyName);
Objects.requireNonNull(pred);
return addBundleVerifier(cmd -> {
final String value;
if (TKit.isLinux()) {
if (isOfType(cmd, LINUX)) {
value = LinuxHelper.getBundleProperty(cmd, propertyName);
} else if (TKit.isWindows()) {
} else if (isOfType(cmd, WINDOWS)) {
value = WindowsHelper.getMsiProperty(cmd, propertyName);
} else {
throw new IllegalStateException();
@ -223,19 +233,23 @@ public final class PackageTest extends RunnablePackageTest {
}
public PackageTest disablePackageInstaller() {
currentTypes.forEach(
type -> packageHandlers.get(type).installHandler = cmd -> {});
currentTypes.forEach(disabledInstallers::add);
return this;
}
public PackageTest disablePackageUninstaller() {
currentTypes.forEach(
type -> packageHandlers.get(type).uninstallHandler = cmd -> {});
currentTypes.forEach(disabledUninstallers::add);
return this;
}
public PackageTest createMsiLog(boolean v) {
createMsiLog = v;
return this;
}
static void withFileAssociationsTestRuns(FileAssociations fa,
ThrowingBiConsumer<FileAssociations.TestRun, List<Path>> consumer) {
Objects.requireNonNull(consumer);
for (var testRun : fa.getTestRuns()) {
TKit.withTempDirectory("fa-test-files", tempDir -> {
List<Path> testFiles = StreamSupport.stream(testRun.getFileNames().spliterator(), false).map(fname -> {
@ -254,6 +268,7 @@ public final class PackageTest extends RunnablePackageTest {
}
PackageTest addHelloAppFileAssociationsVerifier(FileAssociations fa) {
Objects.requireNonNull(fa);
// Setup test app to have valid jpackage command line before
// running check of type of environment.
@ -290,7 +305,7 @@ public final class PackageTest extends RunnablePackageTest {
Collections.emptyMap());
});
if (TKit.isWindows()) {
if (isOfType(cmd, WINDOWS)) {
// Verify context menu label in registry.
String progId = WindowsHelper.queryRegistryValue(
String.format("HKEY_LOCAL_MACHINE\\SOFTWARE\\Classes\\%s", fa.getSuffix()), "");
@ -307,8 +322,7 @@ public final class PackageTest extends RunnablePackageTest {
}
public PackageTest forTypes(Collection<PackageType> types, Runnable action) {
Set<PackageType> oldTypes = Set.of(currentTypes.toArray(
PackageType[]::new));
final var oldTypes = Set.of(currentTypes.toArray(PackageType[]::new));
try {
forTypes(types);
action.run();
@ -374,10 +388,59 @@ public final class PackageTest extends RunnablePackageTest {
private final List<Consumer<Action>> handlers;
}
static final class PackageHandlers {
Consumer<JPackageCommand> installHandler;
Consumer<JPackageCommand> uninstallHandler;
BiFunction<JPackageCommand, Path, Path> unpackHandler;
PackageTest packageHandlers(PackageHandlers v) {
Objects.requireNonNull(v);
currentTypes.forEach(type -> packageHandlers.put(type, v));
return this;
}
PackageTest isPackageTypeSupported(Predicate<PackageType> v) {
Objects.requireNonNull(v);
isPackageTypeSupported = v;
return this;
}
PackageTest jpackageFactory(Supplier<JPackageCommand> v) {
Objects.requireNonNull(v);
jpackageFactory = v;
return this;
}
record PackageHandlers(Function<JPackageCommand, Integer> installHandler,
Consumer<JPackageCommand> uninstallHandler,
Optional<? extends BiFunction<JPackageCommand, Path, Path>> unpackHandler) {
PackageHandlers(Function<JPackageCommand, Integer> installHandler,
Consumer<JPackageCommand> uninstallHandler,
BiFunction<JPackageCommand, Path, Path> unpackHandler) {
this(installHandler, uninstallHandler, Optional.of(unpackHandler));
}
PackageHandlers {
Objects.requireNonNull(installHandler);
Objects.requireNonNull(uninstallHandler);
Objects.requireNonNull(unpackHandler);
}
PackageHandlers copyWithNopInstaller() {
return new PackageHandlers(cmd -> 0, uninstallHandler, unpackHandler);
}
PackageHandlers copyWithNopUninstaller() {
return new PackageHandlers(installHandler, cmd -> {}, unpackHandler);
}
int install(JPackageCommand cmd) {
return installHandler.apply(cmd);
}
Path unpack(JPackageCommand cmd, Path unpackDir) {
return unpackHandler.orElseThrow().apply(cmd, unpackDir);
}
void uninstall(JPackageCommand cmd) {
uninstallHandler.accept(cmd);
}
}
@Override
@ -393,164 +456,197 @@ public final class PackageTest extends RunnablePackageTest {
}
private List<Consumer<Action>> createPackageTypeHandlers() {
return NATIVE.stream()
.map(type -> {
Handler handler = handlers.entrySet().stream()
.filter(entry -> !entry.getValue().isVoid())
.filter(entry -> entry.getKey() == type)
.map(entry -> entry.getValue())
.findAny().orElse(null);
Map.Entry<PackageType, Handler> result = null;
if (handler != null) {
result = Map.entry(type, handler);
}
return result;
})
.filter(Objects::nonNull)
.map(entry -> createPackageTypeHandler(
entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
return handlers.entrySet().stream()
.filter(entry -> !entry.getValue().isVoid())
.filter(entry -> NATIVE.contains(entry.getKey()))
.sorted(Comparator.comparing(Map.Entry::getKey))
.map(entry -> {
return createPackageTypeHandler(entry.getKey(), entry.getValue());
}).toList();
}
private Consumer<Action> createPackageTypeHandler(
PackageType type, Handler handler) {
return toConsumer(new ThrowingConsumer<Action>() {
@Override
public void accept(Action action) throws Throwable {
if (terminated) {
throw new IllegalStateException();
private record PackageTypePipeline(PackageType type, int expectedJPackageExitCode,
int expectedInstallExitCode, PackageHandlers packageHandlers, Handler handler,
JPackageCommand cmd, State state) implements Consumer<Action> {
PackageTypePipeline {
Objects.requireNonNull(type);
Objects.requireNonNull(packageHandlers);
Objects.requireNonNull(handler);
Objects.requireNonNull(cmd);
Objects.requireNonNull(state);
}
PackageTypePipeline(PackageType type, int expectedJPackageExitCode,
int expectedInstallExitCode, PackageHandlers packageHandlers,
Handler handler, JPackageCommand cmd) {
this(type, expectedJPackageExitCode, expectedInstallExitCode,
packageHandlers, handler, cmd, new State());
}
@Override
public void accept(Action action) {
switch(analizeAction(action)) {
case SKIP_NO_PACKAGE_HANDLER -> {
TKit.trace(String.format("No handler of [%s] action for %s command",
action, cmd.getPrintableCommandLine()));
return;
}
if (action == Action.FINALIZE) {
if (unpackDir != null) {
if (Files.isDirectory(unpackDir)
&& !unpackDir.startsWith(TKit.workDir())) {
TKit.deleteDirectoryRecursive(unpackDir);
}
unpackDir = null;
}
terminated = true;
}
boolean skip = false;
if (unhandledAction != null) {
switch (unhandledAction) {
case CREATE:
skip = true;
break;
case UNPACK:
case INSTALL:
skip = (action == Action.VERIFY_INSTALL);
break;
case UNINSTALL:
skip = (action == Action.VERIFY_UNINSTALL);
break;
default: // NOP
}
}
if (skip) {
case SKIP -> {
TKit.trace(String.format("Skip [%s] action of %s command",
action, cmd.getPrintableCommandLine()));
return;
}
final Supplier<JPackageCommand> curCmd = () -> {
if (Set.of(Action.INITIALIZE, Action.CREATE).contains(action)) {
return cmd;
} else {
return cmd.createImmutableCopy();
}
};
switch (action) {
case UNPACK: {
cmd.setUnpackedPackageLocation(null);
handleAction(action,
packageHandlers.get(type).unpackHandler,
handler -> {
unpackDir = TKit.createTempDirectory(
String.format("unpacked-%s",
type.getName()));
unpackDir = handler.apply(cmd, unpackDir);
cmd.setUnpackedPackageLocation(unpackDir);
});
break;
}
case INSTALL: {
cmd.setUnpackedPackageLocation(null);
handleAction(action,
packageHandlers.get(type).installHandler,
handler -> {
handler.accept(curCmd.get());
});
break;
}
case UNINSTALL: {
handleAction(action,
packageHandlers.get(type).uninstallHandler,
handler -> {
handler.accept(curCmd.get());
});
break;
}
case CREATE:
cmd.setUnpackedPackageLocation(null);
handler.accept(action, curCmd.get());
handleAction(action,
(expectedJPackageExitCode == 0) ? Boolean.TRUE : null,
handler -> {
});
return;
default:
handler.accept(action, curCmd.get());
break;
}
Optional.ofNullable(unhandledAction).ifPresent(v -> {
TKit.trace(String.format(
"No handler of [%s] action for %s command", v,
cmd.getPrintableCommandLine()));
});
}
private <T> void handleAction(Action action, T handler,
ThrowingConsumer<T> consumer) throws Throwable {
if (handler == null) {
unhandledAction = action;
} else {
unhandledAction = null;
consumer.accept(handler);
case PROCESS -> {
}
}
private Path unpackDir;
private Action unhandledAction;
private boolean terminated;
private final JPackageCommand cmd = Functional.identity(() -> {
JPackageCommand result = new JPackageCommand();
result.setDefaultInputOutput().setDefaultAppName();
if (BUNDLE_OUTPUT_DIR != null && !ignoreBundleOutputDir) {
result.setArgumentValue("--dest", BUNDLE_OUTPUT_DIR.toString());
switch (action) {
case UNPACK -> {
cmd.setUnpackedPackageLocation(null);
final var unpackRootDir = TKit.createTempDirectory(
String.format("unpacked-%s", type.getName()));
final Path unpackDir = packageHandlers.unpack(cmd, unpackRootDir);
if (!unpackDir.startsWith(TKit.workDir())) {
state.deleteUnpackDirs.add(unpackDir);
}
cmd.setUnpackedPackageLocation(unpackDir);
}
type.applyTo(result);
return result;
}).get();
});
case INSTALL -> {
cmd.setUnpackedPackageLocation(null);
final int installExitCode = packageHandlers.install(cmd);
TKit.assertEquals(expectedInstallExitCode, installExitCode,
String.format("Check installer exited with %d code", expectedInstallExitCode));
}
case UNINSTALL -> {
cmd.setUnpackedPackageLocation(null);
packageHandlers.uninstall(cmd);
}
case CREATE -> {
cmd.setUnpackedPackageLocation(null);
handler.processAction(action, cmd, expectedJPackageExitCode);
}
case INITIALIZE -> {
handler.processAction(action, cmd, expectedJPackageExitCode);
}
case FINALIZE -> {
state.deleteUnpackDirs.forEach(TKit::deleteDirectoryRecursive);
state.deleteUnpackDirs.clear();
}
default -> {
handler.processAction(action, cmd.createImmutableCopy(), expectedJPackageExitCode);
}
}
}
private enum ActionAction {
PROCESS,
SKIP,
SKIP_NO_PACKAGE_HANDLER
}
private ActionAction analizeAction(Action action) {
Objects.requireNonNull(action);
if (jpackageFailed()) {
return ActionAction.SKIP;
}
switch (action) {
case CREATE -> {
state.packageActions.add(action);
}
case INSTALL -> {
state.packageActions.add(action);
state.packageActions.remove(Action.UNPACK);
}
case UNINSTALL -> {
state.packageActions.add(action);
if (installFailed()) {
return ActionAction.SKIP;
}
}
case UNPACK -> {
state.packageActions.add(action);
state.packageActions.remove(Action.INSTALL);
if (unpackNotSupported()) {
return ActionAction.SKIP_NO_PACKAGE_HANDLER;
}
}
case VERIFY_INSTALL -> {
if (unpackNotSupported()) {
return ActionAction.SKIP;
}
if (installFailed()) {
return ActionAction.SKIP;
}
}
case VERIFY_UNINSTALL -> {
if (installFailed() && processed(Action.UNINSTALL)) {
return ActionAction.SKIP;
}
}
default -> {
// NOP
}
}
return ActionAction.PROCESS;
}
private boolean processed(Action action) {
Objects.requireNonNull(action);
return state.packageActions.contains(action);
}
private boolean installFailed() {
return processed(Action.INSTALL) && expectedInstallExitCode != 0;
}
private boolean jpackageFailed() {
return processed(Action.CREATE) && expectedJPackageExitCode != 0;
}
private boolean unpackNotSupported() {
return processed(Action.UNPACK) && packageHandlers.unpackHandler().isEmpty();
}
private final static class State {
private final Set<Action> packageActions = new HashSet<>();
private final List<Path> deleteUnpackDirs = new ArrayList<>();
}
}
private class Handler implements BiConsumer<Action, JPackageCommand> {
private Consumer<Action> createPackageTypeHandler(PackageType type, Handler handler) {
final var cmd = jpackageFactory.get();
cmd.setDefaultInputOutput().setDefaultAppName();
if (BUNDLE_OUTPUT_DIR != null && !ignoreBundleOutputDir) {
cmd.setArgumentValue("--dest", BUNDLE_OUTPUT_DIR.toString());
}
type.applyTo(cmd);
return new PackageTypePipeline(type, expectedJPackageExitCode,
expectedInstallExitCode, getPackageHandlers(type), handler.copy(), cmd);
}
private record Handler(List<Consumer<JPackageCommand>> initializers,
List<BiConsumer<JPackageCommand, Executor.Result>> bundleVerifiers,
List<Consumer<JPackageCommand>> installVerifiers,
List<Consumer<JPackageCommand>> uninstallVerifiers) {
Handler() {
initializers = new ArrayList<>();
bundleVerifiers = new ArrayList<>();
installVerifiers = new ArrayList<>();
uninstallVerifiers = new ArrayList<>();
this(new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
}
Handler copy() {
return new Handler(List.copyOf(initializers), List.copyOf(bundleVerifiers),
List.copyOf(installVerifiers), List.copyOf(uninstallVerifiers));
}
boolean isVoid() {
@ -573,18 +669,17 @@ public final class PackageTest extends RunnablePackageTest {
uninstallVerifiers.add(v);
}
@Override
public void accept(Action action, JPackageCommand cmd) {
public void processAction(Action action, JPackageCommand cmd, int expectedJPackageExitCode) {
switch (action) {
case INITIALIZE:
case INITIALIZE -> {
initializers.forEach(v -> v.accept(cmd));
if (cmd.isImagePackageType()) {
throw new UnsupportedOperationException();
}
cmd.executePrerequisiteActions();
break;
}
case CREATE:
case CREATE -> {
Executor.Result result = cmd.execute(expectedJPackageExitCode);
if (expectedJPackageExitCode == 0) {
TKit.assertFileExists(cmd.outputBundle());
@ -593,39 +688,38 @@ public final class PackageTest extends RunnablePackageTest {
TKit.assertPathExists(outputBundle, false);
});
}
verifyPackageBundle(cmd, result);
break;
verifyPackageBundle(cmd, result, expectedJPackageExitCode);
}
case VERIFY_INSTALL:
case VERIFY_INSTALL -> {
if (expectedJPackageExitCode == 0) {
verifyPackageInstalled(cmd);
}
break;
}
case VERIFY_UNINSTALL:
case VERIFY_UNINSTALL -> {
if (expectedJPackageExitCode == 0) {
verifyPackageUninstalled(cmd);
}
break;
}
case PURGE:
if (expectedJPackageExitCode == 0) {
var bundle = cmd.outputBundle();
if (toSupplier(() -> TKit.deleteIfExists(bundle)).get()) {
TKit.trace(String.format("Deleted [%s] package",
bundle));
}
case PURGE -> {
var bundle = cmd.outputBundle();
if (toSupplier(() -> TKit.deleteIfExists(bundle)).get()) {
TKit.trace(String.format("Deleted [%s] package", bundle));
}
break;
}
default: // NOP
default -> {
// NOP
}
}
}
private void verifyPackageBundle(JPackageCommand cmd,
Executor.Result result) {
Executor.Result result, int expectedJPackageExitCode) {
if (expectedJPackageExitCode == 0) {
if (LINUX.contains(cmd.packageType())) {
if (isOfType(cmd, LINUX)) {
LinuxHelper.verifyPackageBundleEssential(cmd);
}
}
@ -647,9 +741,7 @@ public final class PackageTest extends RunnablePackageTest {
});
if (!cmd.isRuntime()) {
if (WINDOWS.contains(cmd.packageType())
&& !cmd.isPackageUnpacked(
"Not verifying desktop integration")) {
if (isOfType(cmd, WINDOWS) && !cmd.isPackageUnpacked("Not verifying desktop integration")) {
// Check main launcher
WindowsHelper.verifyDesktopIntegration(cmd, null);
// Check additional launchers
@ -659,8 +751,7 @@ public final class PackageTest extends RunnablePackageTest {
}
}
if (LauncherAsServiceVerifier.SUPPORTED_PACKAGES.contains(
cmd.packageType())) {
if (isOfType(cmd, LauncherAsServiceVerifier.SUPPORTED_PACKAGES)) {
LauncherAsServiceVerifier.verify(cmd);
}
@ -676,13 +767,13 @@ public final class PackageTest extends RunnablePackageTest {
&& !LauncherAsServiceVerifier.getLaunchersAsServices(cmd).isEmpty();
final long expectedRootCount;
if (WINDOWS.contains(cmd.packageType())) {
if (isOfType(cmd, WINDOWS)) {
// On Windows it is always two entries:
// installation home directory and MSI file
expectedRootCount = 2;
} else if (withServices && MAC_PKG.equals(cmd.packageType())) {
} else if (withServices && isOfType(cmd, MAC_PKG)) {
expectedRootCount = 2;
} else if (LINUX.contains(cmd.packageType())) {
} else if (isOfType(cmd, LINUX)) {
Set<Path> roots = new HashSet<>();
roots.add(Path.of("/").resolve(Path.of(cmd.getArgumentValue(
"--install-dir", () -> "/opt")).getName(0)));
@ -732,7 +823,7 @@ public final class PackageTest extends RunnablePackageTest {
if (!cmd.isRuntime()) {
TKit.assertPathExists(cmd.appLauncherPath(), false);
if (WINDOWS.contains(cmd.packageType())) {
if (isOfType(cmd, WINDOWS)) {
// Check main launcher
WindowsHelper.verifyDesktopIntegration(cmd, null);
// Check additional launchers
@ -743,54 +834,94 @@ public final class PackageTest extends RunnablePackageTest {
}
Path appInstallDir = cmd.appInstallationDirectory();
if (TKit.isLinux() && Path.of("/").equals(appInstallDir)) {
if (isOfType(cmd, LINUX) && Path.of("/").equals(appInstallDir)) {
ApplicationLayout appLayout = cmd.appLayout();
TKit.assertPathExists(appLayout.runtimeDirectory(), false);
} else {
TKit.assertPathExists(appInstallDir, false);
}
if (LauncherAsServiceVerifier.SUPPORTED_PACKAGES.contains(
cmd.packageType())) {
if (isOfType(cmd, LauncherAsServiceVerifier.SUPPORTED_PACKAGES)) {
LauncherAsServiceVerifier.verifyUninstalled(cmd);
}
uninstallVerifiers.forEach(v -> v.accept(cmd));
}
private final List<Consumer<JPackageCommand>> initializers;
private final List<BiConsumer<JPackageCommand, Executor.Result>> bundleVerifiers;
private final List<Consumer<JPackageCommand>> installVerifiers;
private final List<Consumer<JPackageCommand>> uninstallVerifiers;
}
private static Map<PackageType, PackageHandlers> createDefaultPackageHandlers() {
HashMap<PackageType, PackageHandlers> handlers = new HashMap<>();
if (TKit.isLinux()) {
handlers.put(LINUX_DEB, LinuxHelper.createDebPackageHandlers());
handlers.put(LINUX_RPM, LinuxHelper.createRpmPackageHandlers());
private PackageHandlers getDefaultPackageHandlers(PackageType type) {
switch (type) {
case LINUX_DEB -> {
return LinuxHelper.createDebPackageHandlers();
}
case LINUX_RPM -> {
return LinuxHelper.createRpmPackageHandlers();
}
case WIN_MSI -> {
return WindowsHelper.createMsiPackageHandlers(createMsiLog);
}
case WIN_EXE -> {
return WindowsHelper.createExePackageHandlers(createMsiLog);
}
case MAC_DMG -> {
return MacHelper.createDmgPackageHandlers();
}
case MAC_PKG -> {
return MacHelper.createPkgPackageHandlers();
}
default -> {
throw new IllegalArgumentException();
}
}
}
private PackageHandlers getPackageHandlers(PackageType type) {
Objects.requireNonNull(type);
var reply = Optional.ofNullable(packageHandlers.get(type)).orElseGet(() -> {
if (TKit.isLinux() && !PackageType.LINUX.contains(type)) {
throw new IllegalArgumentException();
} else if (TKit.isWindows() && !PackageType.WINDOWS.contains(type)) {
throw new IllegalArgumentException();
} else if (TKit.isOSX() && !PackageType.MAC.contains(type)) {
throw new IllegalArgumentException();
} else {
return getDefaultPackageHandlers(type);
}
});
if (disabledInstallers.contains(type)) {
reply = reply.copyWithNopInstaller();
}
if (TKit.isWindows()) {
handlers.put(WIN_MSI, WindowsHelper.createMsiPackageHandlers());
handlers.put(WIN_EXE, WindowsHelper.createExePackageHandlers());
if (disabledUninstallers.contains(type)) {
reply = reply.copyWithNopUninstaller();
}
if (TKit.isOSX()) {
handlers.put(MAC_DMG, MacHelper.createDmgPackageHandlers());
handlers.put(MAC_PKG, MacHelper.createPkgPackageHandlers());
}
return reply;
}
return handlers;
private static boolean isOfType(JPackageCommand cmd, PackageType packageTypes) {
return isOfType(cmd, Set.of(packageTypes));
}
private static boolean isOfType(JPackageCommand cmd, Set<PackageType> packageTypes) {
return Optional.ofNullable(cmd.packageType()).map(packageTypes::contains).orElse(false);
}
private Collection<PackageType> currentTypes;
private Set<PackageType> excludeTypes;
private int expectedJPackageExitCode;
private Map<PackageType, Handler> handlers;
private Set<String> namedInitializers;
private Map<PackageType, PackageHandlers> packageHandlers;
private int expectedInstallExitCode;
private final Map<PackageType, Handler> handlers;
private final Set<String> namedInitializers;
private final Map<PackageType, PackageHandlers> packageHandlers;
private final Set<PackageType> disabledInstallers;
private final Set<PackageType> disabledUninstallers;
private Predicate<PackageType> isPackageTypeSupported;
private Supplier<JPackageCommand> jpackageFactory;
private boolean ignoreBundleOutputDir;
private boolean createMsiLog;
private static final Path BUNDLE_OUTPUT_DIR;

View File

@ -22,24 +22,25 @@
*/
package jdk.jpackage.test;
import static jdk.jpackage.internal.util.function.ExceptionBox.rethrowUnchecked;
import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static jdk.jpackage.internal.util.function.ExceptionBox.rethrowUnchecked;
import jdk.jpackage.internal.util.function.ThrowingRunnable;
import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier;
import jdk.jpackage.test.PackageTest.PackageHandlers;
public class WindowsHelper {
@ -67,21 +68,24 @@ public class WindowsHelper {
return Path.of(cmd.getArgumentValue("--install-dir", cmd::name));
}
private static void runMsiexecWithRetries(Executor misexec) {
private static int runMsiexecWithRetries(Executor misexec, Optional<Path> msiLog) {
Executor.Result result = null;
final boolean isUnpack = misexec.getExecutable().orElseThrow().equals(Path.of("cmd"));
final List<String> origArgs = msiLog.isPresent() ? misexec.getAllArguments() : null;
for (int attempt = 0; attempt < 8; ++attempt) {
msiLog.ifPresent(v -> misexec.clearArguments().addArguments(origArgs).addArgument("/L*v").addArgument(v));
result = misexec.executeWithoutExitCodeCheck();
if (result.exitCode() == 1605) {
// ERROR_UNKNOWN_PRODUCT, attempt to uninstall not installed
// package
return;
return result.exitCode();
}
// The given Executor may either be of an msiexec command or an
// unpack.bat script containing the msiexec command. In the later
// case, when misexec returns 1618, the unpack.bat may return 1603
if ((result.exitCode() == 1618) || (result.exitCode() == 1603)) {
if ((result.exitCode() == 1618) || (result.exitCode() == 1603 && isUnpack)) {
// Another installation is already in progress.
// Wait a little and try again.
Long timeout = 1000L * (attempt + 3); // from 3 to 10 seconds
@ -91,74 +95,123 @@ public class WindowsHelper {
break;
}
result.assertExitCodeIsZero();
return result.exitCode();
}
static PackageHandlers createMsiPackageHandlers() {
BiConsumer<JPackageCommand, Boolean> installMsi = (cmd, install) -> {
cmd.verifyIsOfType(PackageType.WIN_MSI);
var msiPath = TransientMsi.create(cmd).path();
runMsiexecWithRetries(Executor.of("msiexec", "/qn", "/norestart",
install ? "/i" : "/x").addArgument(msiPath));
};
static PackageHandlers createMsiPackageHandlers(boolean createMsiLog) {
return new PackageHandlers(cmd -> installMsi(cmd, createMsiLog),
cmd -> uninstallMsi(cmd, createMsiLog), WindowsHelper::unpackMsi);
}
PackageHandlers msi = new PackageHandlers();
msi.installHandler = cmd -> installMsi.accept(cmd, true);
msi.uninstallHandler = cmd -> {
if (Files.exists(cmd.outputBundle())) {
installMsi.accept(cmd, false);
private static Optional<Path> configureMsiLogFile(JPackageCommand cmd, boolean createMsiLog) {
final Optional<Path> msiLogFile;
if (createMsiLog) {
msiLogFile = Optional.of(TKit.createTempFile(String.format("logs\\%s-msi.log",
cmd.packageType().getName())));
} else {
msiLogFile = Optional.empty();
}
cmd.winMsiLogFile(msiLogFile.orElse(null));
return msiLogFile;
}
private static int runMsiInstaller(JPackageCommand cmd, boolean createMsiLog, boolean install) {
cmd.verifyIsOfType(PackageType.WIN_MSI);
final var msiPath = TransientMsi.create(cmd).path();
return runMsiexecWithRetries(Executor.of("msiexec", "/qn", "/norestart",
install ? "/i" : "/x").addArgument(msiPath), configureMsiLogFile(cmd, createMsiLog));
}
private static int installMsi(JPackageCommand cmd, boolean createMsiLog) {
return runMsiInstaller(cmd, createMsiLog, true);
}
private static void uninstallMsi(JPackageCommand cmd, boolean createMsiLog) {
if (Files.exists(cmd.outputBundle())) {
runMsiInstaller(cmd, createMsiLog, false);
} else {
configureMsiLogFile(cmd, false);
}
}
private static Path unpackMsi(JPackageCommand cmd, Path destinationDir) {
cmd.verifyIsOfType(PackageType.WIN_MSI);
configureMsiLogFile(cmd, false);
final Path unpackBat = destinationDir.resolve("unpack.bat");
final Path unpackDir = destinationDir.resolve(
TKit.removeRootFromAbsolutePath(
getInstallationRootDirectory(cmd)));
final Path msiPath = TransientMsi.create(cmd).path();
// Put msiexec in .bat file because can't pass value of TARGETDIR
// property containing spaces through ProcessBuilder properly.
// Set folder permissions to allow msiexec unpack msi bundle.
TKit.createTextFile(unpackBat, List.of(
String.format("icacls \"%s\" /inheritance:e /grant Users:M",
destinationDir),
String.join(" ", List.of(
"msiexec",
"/a",
String.format("\"%s\"", msiPath),
"/qn",
String.format("TARGETDIR=\"%s\"",
unpackDir.toAbsolutePath().normalize())))));
runMsiexecWithRetries(Executor.of("cmd", "/c", unpackBat.toString()), Optional.empty());
//
// WiX3 uses "." as the value of "DefaultDir" field for "ProgramFiles64Folder" folder in msi's Directory table
// WiX4 uses "PFiles64" as the value of "DefaultDir" field for "ProgramFiles64Folder" folder in msi's Directory table
// msiexec creates "Program Files/./<App Installation Directory>" from WiX3 msi which translates to "Program Files/<App Installation Directory>"
// msiexec creates "Program Files/PFiles64/<App Installation Directory>" from WiX4 msi
// So for WiX4 msi we need to transform "Program Files/PFiles64/<App Installation Directory>" into "Program Files/<App Installation Directory>"
//
// WiX4 does the same thing for %LocalAppData%.
//
for (var extraPathComponent : List.of("PFiles64", "LocalApp")) {
if (Files.isDirectory(unpackDir.resolve(extraPathComponent))) {
Path installationSubDirectory = getInstallationSubDirectory(cmd);
Path from = Path.of(extraPathComponent).resolve(installationSubDirectory);
Path to = installationSubDirectory;
TKit.trace(String.format("Convert [%s] into [%s] in [%s] directory", from, to,
unpackDir));
ThrowingRunnable.toRunnable(() -> {
Files.createDirectories(unpackDir.resolve(to).getParent());
Files.move(unpackDir.resolve(from), unpackDir.resolve(to));
TKit.deleteDirectoryRecursive(unpackDir.resolve(extraPathComponent));
}).run();
}
};
msi.unpackHandler = (cmd, destinationDir) -> {
cmd.verifyIsOfType(PackageType.WIN_MSI);
final Path unpackBat = destinationDir.resolve("unpack.bat");
final Path unpackDir = destinationDir.resolve(
TKit.removeRootFromAbsolutePath(
getInstallationRootDirectory(cmd)));
}
return destinationDir;
}
final Path msiPath = TransientMsi.create(cmd).path();
static PackageHandlers createExePackageHandlers(boolean createMsiLog) {
return new PackageHandlers(cmd -> installExe(cmd, createMsiLog), WindowsHelper::uninstallExe, Optional.empty());
}
// Put msiexec in .bat file because can't pass value of TARGETDIR
// property containing spaces through ProcessBuilder properly.
// Set folder permissions to allow msiexec unpack msi bundle.
TKit.createTextFile(unpackBat, List.of(
String.format("icacls \"%s\" /inheritance:e /grant Users:M",
destinationDir),
String.join(" ", List.of(
"msiexec",
"/a",
String.format("\"%s\"", msiPath),
"/qn",
String.format("TARGETDIR=\"%s\"",
unpackDir.toAbsolutePath().normalize())))));
runMsiexecWithRetries(Executor.of("cmd", "/c", unpackBat.toString()));
private static int runExeInstaller(JPackageCommand cmd, boolean createMsiLog, boolean install) {
cmd.verifyIsOfType(PackageType.WIN_EXE);
Executor exec = new Executor().setExecutable(cmd.outputBundle());
if (install) {
exec.addArgument("/qn").addArgument("/norestart");
} else {
exec.addArgument("uninstall");
}
return runMsiexecWithRetries(exec, configureMsiLogFile(cmd, createMsiLog));
}
//
// WiX3 uses "." as the value of "DefaultDir" field for "ProgramFiles64Folder" folder in msi's Directory table
// WiX4 uses "PFiles64" as the value of "DefaultDir" field for "ProgramFiles64Folder" folder in msi's Directory table
// msiexec creates "Program Files/./<App Installation Directory>" from WiX3 msi which translates to "Program Files/<App Installation Directory>"
// msiexec creates "Program Files/PFiles64/<App Installation Directory>" from WiX4 msi
// So for WiX4 msi we need to transform "Program Files/PFiles64/<App Installation Directory>" into "Program Files/<App Installation Directory>"
//
// WiX4 does the same thing for %LocalAppData%.
//
for (var extraPathComponent : List.of("PFiles64", "LocalApp")) {
if (Files.isDirectory(unpackDir.resolve(extraPathComponent))) {
Path installationSubDirectory = getInstallationSubDirectory(cmd);
Path from = Path.of(extraPathComponent).resolve(installationSubDirectory);
Path to = installationSubDirectory;
TKit.trace(String.format("Convert [%s] into [%s] in [%s] directory", from, to,
unpackDir));
ThrowingRunnable.toRunnable(() -> {
Files.createDirectories(unpackDir.resolve(to).getParent());
Files.move(unpackDir.resolve(from), unpackDir.resolve(to));
TKit.deleteDirectoryRecursive(unpackDir.resolve(extraPathComponent));
}).run();
}
}
return destinationDir;
};
return msi;
private static int installExe(JPackageCommand cmd, boolean createMsiLog) {
return runExeInstaller(cmd, createMsiLog, true);
}
private static void uninstallExe(JPackageCommand cmd) {
if (Files.exists(cmd.outputBundle())) {
runExeInstaller(cmd, false, false);
} else {
configureMsiLogFile(cmd, false);
}
}
record TransientMsi(Path path) {
@ -204,28 +257,6 @@ public class WindowsHelper {
}
}
static PackageHandlers createExePackageHandlers() {
BiConsumer<JPackageCommand, Boolean> installExe = (cmd, install) -> {
cmd.verifyIsOfType(PackageType.WIN_EXE);
Executor exec = new Executor().setExecutable(cmd.outputBundle());
if (install) {
exec.addArgument("/qn").addArgument("/norestart");
} else {
exec.addArgument("uninstall");
}
runMsiexecWithRetries(exec);
};
PackageHandlers exe = new PackageHandlers();
exe.installHandler = cmd -> installExe.accept(cmd, true);
exe.uninstallHandler = cmd -> {
if (Files.exists(cmd.outputBundle())) {
installExe.accept(cmd, false);
}
};
return exe;
}
static void verifyDesktopIntegration(JPackageCommand cmd,
String launcherName) {
new DesktopIntegrationVerifier(cmd, launcherName);
@ -415,14 +446,12 @@ public class WindowsHelper {
}
private void verifySystemDesktopShortcut(boolean exists) {
Path dir = Path.of(queryRegistryValueCache(
SYSTEM_SHELL_FOLDERS_REGKEY, "Common Desktop"));
Path dir = SpecialFolder.COMMON_DESKTOP.getPath();
verifyShortcut(dir.resolve(desktopShortcutPath), exists);
}
private void verifyUserLocalDesktopShortcut(boolean exists) {
Path dir = Path.of(
queryRegistryValueCache(USER_SHELL_FOLDERS_REGKEY, "Desktop"));
Path dir = SpecialFolder.USER_DESKTOP.getPath();
verifyShortcut(dir.resolve(desktopShortcutPath), exists);
}
@ -445,19 +474,22 @@ public class WindowsHelper {
Path shortcutPath = shortcutsRoot.resolve(startMenuShortcutPath);
verifyShortcut(shortcutPath, exists);
if (!exists) {
TKit.assertDirectoryNotEmpty(shortcutPath.getParent());
final var parentDir = shortcutPath.getParent();
if (Files.isDirectory(parentDir)) {
TKit.assertDirectoryNotEmpty(parentDir);
} else {
TKit.assertPathExists(parentDir, false);
}
}
}
private void verifySystemStartMenuShortcut(boolean exists) {
verifyStartMenuShortcut(Path.of(queryRegistryValueCache(
SYSTEM_SHELL_FOLDERS_REGKEY, "Common Programs")), exists);
verifyStartMenuShortcut(SpecialFolder.COMMON_START_MENU_PROGRAMS.getPath(), exists);
}
private void verifyUserLocalStartMenuShortcut(boolean exists) {
verifyStartMenuShortcut(Path.of(queryRegistryValueCache(
USER_SHELL_FOLDERS_REGKEY, "Programs")), exists);
verifyStartMenuShortcut(SpecialFolder.USER_START_MENU_PROGRAMS.getPath(), exists);
}
private void verifyFileAssociationsRegistry(Path faFile) {
@ -565,16 +597,66 @@ public class WindowsHelper {
return value;
}
private static String queryRegistryValueCache(String keyPath,
String valueName) {
String key = String.format("[%s][%s]", keyPath, valueName);
String value = REGISTRY_VALUES.get(key);
if (value == null) {
value = queryRegistryValue(keyPath, valueName);
REGISTRY_VALUES.put(key, value);
// See .NET special folders
private enum SpecialFolderDotNet {
Desktop,
CommonDesktop,
Programs,
CommonPrograms;
Path getPath() {
final var str = Executor.of("powershell", "-NoLogo", "-NoProfile",
"-NonInteractive", "-Command",
String.format("[Environment]::GetFolderPath('%s')", name())
).saveFirstLineOfOutput().execute().getFirstLineOfOutput();
TKit.trace(String.format("Value of .NET special folder '%s' is [%s]", name(), str));
return Path.of(str);
}
}
private record RegValuePath(String keyPath, String valueName) {
RegValuePath {
Objects.requireNonNull(keyPath);
Objects.requireNonNull(valueName);
}
return value;
Optional<String> findValue() {
return Optional.ofNullable(queryRegistryValue(keyPath, valueName));
}
}
private enum SpecialFolder {
COMMON_START_MENU_PROGRAMS(SYSTEM_SHELL_FOLDERS_REGKEY, "Common Programs", SpecialFolderDotNet.CommonPrograms),
USER_START_MENU_PROGRAMS(USER_SHELL_FOLDERS_REGKEY, "Programs", SpecialFolderDotNet.Programs),
COMMON_DESKTOP(SYSTEM_SHELL_FOLDERS_REGKEY, "Common Desktop", SpecialFolderDotNet.CommonDesktop),
USER_DESKTOP(USER_SHELL_FOLDERS_REGKEY, "Desktop", SpecialFolderDotNet.Desktop);
SpecialFolder(String keyPath, String valueName) {
reg = new RegValuePath(keyPath, valueName);
alt = Optional.empty();
}
SpecialFolder(String keyPath, String valueName, SpecialFolderDotNet alt) {
reg = new RegValuePath(keyPath, valueName);
this.alt = Optional.of(alt);
}
Path getPath() {
return CACHE.computeIfAbsent(this, k -> reg.findValue().map(Path::of).orElseGet(() -> {
return alt.map(SpecialFolderDotNet::getPath).orElseThrow(() -> {
return new NoSuchElementException(String.format("Failed to find path to %s folder", name()));
});
}));
}
private final RegValuePath reg;
private final Optional<SpecialFolderDotNet> alt;
private final static Map<SpecialFolder, Path> CACHE = new ConcurrentHashMap<>();
}
private static final class ShortPathUtils {
@ -617,7 +699,5 @@ public class WindowsHelper {
private static final String SYSTEM_SHELL_FOLDERS_REGKEY = "HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders";
private static final String USER_SHELL_FOLDERS_REGKEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders";
private static final Map<String, String> REGISTRY_VALUES = new HashMap<>();
private static final int WIN_MAX_PATH = 260;
}

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
* Copyright (c) 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
* 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.
*/
-->
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Fragment>
<Condition Message="Not supported on this version of Windows">0</Condition>
<ComponentGroup Id="FragmentOsCondition"/>
</Fragment>
</Wix>

View File

@ -0,0 +1,77 @@
/*
* Copyright (c) 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
* 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 jdk.jpackage.test.Annotations.Test;
import jdk.jpackage.test.JPackageCommand;
import jdk.jpackage.test.PackageTest;
import jdk.jpackage.test.PackageType;
import jdk.jpackage.test.RunnablePackageTest.Action;
import jdk.jpackage.test.TKit;
/*
* @test
* @summary jpackage test that installer blocks on Windows of older version
* @library /test/jdk/tools/jpackage/helpers
* @key jpackagePlatformPackage
* @build jdk.jpackage.test.*
* @compile -Xlint:all -Werror WinOSConditionTest.java
* @requires (os.family == "windows")
* @requires (jpackage.test.SQETest == null)
* @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main
* --jpt-run=WinOSConditionTest
*/
public class WinOSConditionTest {
@Test
public static void test() throws IOException {
// Use custom always failing condition. Installation is expected to fail.
// This way the test covers:
// 1. If jpackage picks custom OS version condition from the resource directory;
// 2. If the installer created by jpackage uses OS version condition.
new PackageTest().ignoreBundleOutputDir()
.forTypes(PackageType.WINDOWS)
.configureHelloApp()
.addInitializer(JPackageCommand::setFakeRuntime)
.addInitializer(cmd -> {
final var resourceDir = TKit.createTempDirectory("resource-dir");
Files.copy(TKit.TEST_SRC_ROOT.resolve("resources/fail-os-condition.wxf"), resourceDir.resolve("os-condition.wxf"));
// Create a per-user installer to let user without admin privileges install it.
cmd.addArguments("--win-per-user-install",
"--resource-dir", resourceDir.toString()).setFakeRuntime();
})
.addUninstallVerifier(cmd -> {
// MSI error code 1603 is generic.
// Dig into the last msi log file for log messages specific to failed condition.
try (final var lines = cmd.winMsiLogFileContents().orElseThrow()) {
TKit.assertTextStream("Doing action: LaunchConditions").predicate(String::endsWith)
.andThen(TKit.assertTextStream("Not supported on this version of Windows").predicate(String::endsWith)).apply(lines);
}
})
.createMsiLog(true)
.setExpectedInstallExitCode(1603)
// Create, try install the package (installation should fail) and verify it is not installed.
.run(Action.CREATE, Action.INSTALL, Action.VERIFY_UNINSTALL);
}
}