8374219: Fix issues in jpackage's Executor class

Reviewed-by: almatvee
This commit is contained in:
Alexey Semenyuk 2026-01-09 22:20:05 +00:00
parent f5fa9e40b0
commit 663a08331a
86 changed files with 8912 additions and 1931 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2026, 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
@ -50,6 +50,7 @@ import jdk.jpackage.internal.model.LinuxLauncher;
import jdk.jpackage.internal.model.LinuxPackage;
import jdk.jpackage.internal.model.Package;
import jdk.jpackage.internal.util.CompositeProxy;
import jdk.jpackage.internal.util.Enquoter;
import jdk.jpackage.internal.util.PathUtils;
import jdk.jpackage.internal.util.XmlUtils;

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2026, 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,13 +27,12 @@ package jdk.jpackage.internal;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Collection;
import java.util.Objects;
import java.util.Collections;
import java.util.Set;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -48,9 +47,6 @@ public final class LibProvidersLookup {
return (new ToolValidator(TOOL_LDD).validate() == null);
}
public LibProvidersLookup() {
}
LibProvidersLookup setPackageLookup(PackageLookup v) {
packageLookup = v;
return this;
@ -87,23 +83,20 @@ public final class LibProvidersLookup {
}
private static List<Path> getNeededLibsForFile(Path path) throws IOException {
List<Path> result = new ArrayList<>();
int ret = Executor.of(TOOL_LDD, path.toString()).setOutputConsumer(lines -> {
lines.map(line -> {
Matcher matcher = LIB_IN_LDD_OUTPUT_REGEX.matcher(line);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}).filter(Objects::nonNull).map(Path::of).forEach(result::add);
}).execute();
final var result = Executor.of(TOOL_LDD, path.toString()).saveOutput().execute();
if (ret != 0) {
if (result.getExitCode() != 0) {
// objdump failed. This is OK if the tool was applied to not a binary file
return Collections.emptyList();
}
return result;
return result.stdout().stream().map(line -> {
Matcher matcher = LIB_IN_LDD_OUTPUT_REGEX.matcher(line);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}).filter(Objects::nonNull).map(Path::of).toList();
}
private static Collection<Path> getNeededLibsForFiles(List<Path> paths) {

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -33,6 +33,7 @@ import static jdk.jpackage.internal.cli.StandardBundlingOperation.CREATE_LINUX_R
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Stream;
import jdk.jpackage.internal.cli.Options;
import jdk.jpackage.internal.cli.StandardBundlingOperation;
@ -44,19 +45,34 @@ import jdk.jpackage.internal.util.Result;
public class LinuxBundlingEnvironment extends DefaultBundlingEnvironment {
public LinuxBundlingEnvironment() {
super(build()
.defaultOperation(() -> {
return LazyLoad.SYS_ENV.value().map(LinuxSystemEnvironment::nativePackageType).map(DESCRIPTORS::get);
})
.bundler(CREATE_LINUX_APP_IMAGE, LinuxBundlingEnvironment::createAppImage)
.bundler(CREATE_LINUX_DEB, LazyLoad::debSysEnv, LinuxBundlingEnvironment::createDebPackage)
.bundler(CREATE_LINUX_RPM, LazyLoad::rpmSysEnv, LinuxBundlingEnvironment::createRpmPackage));
super(build().mutate(builder -> {
// Wrap the generic Linux system environment supplier in the run-once wrapper
// as this supplier is called from both RPM and DEB Linux system environment suppliers.
var sysEnv = runOnce(() -> {
return LinuxSystemEnvironment.create();
});
Supplier<Result<LinuxDebSystemEnvironment>> debSysEnv = () -> {
return LinuxDebSystemEnvironment.create(sysEnv.get());
};
Supplier<Result<LinuxRpmSystemEnvironment>> rpmSysEnv = () -> {
return LinuxRpmSystemEnvironment.create(sysEnv.get());
};
builder.defaultOperation(() -> {
return sysEnv.get().value().map(LinuxSystemEnvironment::nativePackageType).map(DESCRIPTORS::get);
})
.bundler(CREATE_LINUX_DEB, debSysEnv, LinuxBundlingEnvironment::createDebPackage)
.bundler(CREATE_LINUX_RPM, rpmSysEnv, LinuxBundlingEnvironment::createRpmPackage);
}).bundler(CREATE_LINUX_APP_IMAGE, LinuxBundlingEnvironment::createAppImage));
}
private static void createDebPackage(Options options, LinuxDebSystemEnvironment sysEnv) {
createNativePackage(options,
LinuxFromOptions.createLinuxDebPackage(options),
LinuxFromOptions.createLinuxDebPackage(options, sysEnv),
buildEnv()::create,
LinuxBundlingEnvironment::buildPipeline,
(env, pkg, outputDir) -> {
@ -67,7 +83,7 @@ public class LinuxBundlingEnvironment extends DefaultBundlingEnvironment {
private static void createRpmPackage(Options options, LinuxRpmSystemEnvironment sysEnv) {
createNativePackage(options,
LinuxFromOptions.createLinuxRpmPackage(options),
LinuxFromOptions.createLinuxRpmPackage(options, sysEnv),
buildEnv()::create,
LinuxBundlingEnvironment::buildPipeline,
(env, pkg, outputDir) -> {
@ -90,23 +106,6 @@ public class LinuxBundlingEnvironment extends DefaultBundlingEnvironment {
return new BuildEnvFromOptions().predefinedAppImageLayout(APPLICATION_LAYOUT);
}
private static final class LazyLoad {
static Result<LinuxDebSystemEnvironment> debSysEnv() {
return DEB_SYS_ENV;
}
static Result<LinuxRpmSystemEnvironment> rpmSysEnv() {
return RPM_SYS_ENV;
}
private static final Result<LinuxSystemEnvironment> SYS_ENV = LinuxSystemEnvironment.create();
private static final Result<LinuxDebSystemEnvironment> DEB_SYS_ENV = LinuxDebSystemEnvironment.create(SYS_ENV);
private static final Result<LinuxRpmSystemEnvironment> RPM_SYS_ENV = LinuxRpmSystemEnvironment.create(SYS_ENV);
}
private static final Map<PackageType, BundlingOperationDescriptor> DESCRIPTORS = Stream.of(
CREATE_LINUX_DEB,
CREATE_LINUX_RPM

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2012, 2026, 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
@ -25,7 +25,6 @@
package jdk.jpackage.internal;
import static jdk.jpackage.internal.model.StandardPackageType.LINUX_DEB;
import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction;
import java.io.IOException;
@ -76,11 +75,11 @@ final class LinuxDebPackager extends LinuxPackager<LinuxDebPackage> {
try {
// Try the real path first as it works better on newer Ubuntu versions
return findProvidingPackages(realPath, sysEnv.dpkg());
return findProvidingPackages(realPath, sysEnv);
} catch (IOException ex) {
// Try the default path if differ
if (!realPath.equals(file)) {
return findProvidingPackages(file, sysEnv.dpkg());
return findProvidingPackages(file, sysEnv);
} else {
throw ex;
}
@ -107,7 +106,7 @@ final class LinuxDebPackager extends LinuxPackager<LinuxDebPackage> {
properties.forEach(property -> cmdline.add(property.name));
Map<String, String> actualValues = Executor.of(cmdline.toArray(String[]::new))
Map<String, String> actualValues = Executor.of(cmdline)
.saveOutput(true)
.executeExpectSuccess()
.getOutput().stream()
@ -158,9 +157,8 @@ final class LinuxDebPackager extends LinuxPackager<LinuxDebPackage> {
cmdline.addAll(List.of("-b", env.appImageDir().toString(), debFile.toAbsolutePath().toString()));
// run dpkg
RetryExecutor.retryOnKnownErrorMessage(
"semop(1): encountered an error: Invalid argument").execute(
cmdline.toArray(String[]::new));
Executor.of(cmdline).retryOnKnownErrorMessage(
"semop(1): encountered an error: Invalid argument").execute();
Log.verbose(I18N.format("message.output-to-location", debFile.toAbsolutePath()));
}
@ -233,7 +231,7 @@ final class LinuxDebPackager extends LinuxPackager<LinuxDebPackage> {
}
}
private static Stream<String> findProvidingPackages(Path file, Path dpkg) throws IOException {
private static Stream<String> findProvidingPackages(Path file, LinuxDebSystemEnvironment sysEnv) throws IOException {
//
// `dpkg -S` command does glob pattern lookup. If not the absolute path
// to the file is specified it might return mltiple package names.
@ -279,9 +277,9 @@ final class LinuxDebPackager extends LinuxPackager<LinuxDebPackage> {
Set<String> archPackages = new HashSet<>();
Set<String> otherPackages = new HashSet<>();
var debArch = LinuxPackageArch.getValue(LINUX_DEB);
var debArch = sysEnv.packageArch().value();
Executor.of(dpkg.toString(), "-S", file.toString())
Executor.of(sysEnv.dpkg().toString(), "-S", file.toString())
.saveOutput(true).executeExpectSuccess()
.getOutput().forEach(line -> {
Matcher matcher = PACKAGE_NAME_REGEX.matcher(line);

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -28,7 +28,7 @@ import static jdk.jpackage.internal.LinuxSystemEnvironment.mixin;
import jdk.jpackage.internal.util.Result;
public interface LinuxDebSystemEnvironment extends LinuxSystemEnvironment, LinuxDebSystemEnvironmentMixin {
interface LinuxDebSystemEnvironment extends LinuxSystemEnvironment, LinuxDebSystemEnvironmentMixin {
static Result<LinuxDebSystemEnvironment> create(Result<LinuxSystemEnvironment> base) {
return mixin(LinuxDebSystemEnvironment.class, base, LinuxDebSystemEnvironmentMixin::create);

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -29,7 +29,7 @@ import java.util.Objects;
import java.util.stream.Stream;
import jdk.jpackage.internal.util.Result;
public interface LinuxDebSystemEnvironmentMixin {
interface LinuxDebSystemEnvironmentMixin {
Path dpkg();
Path dpkgdeb();
Path fakeroot();

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -70,9 +70,9 @@ final class LinuxFromOptions {
return LinuxApplication.create(appBuilder.create());
}
static LinuxRpmPackage createLinuxRpmPackage(Options options) {
static LinuxRpmPackage createLinuxRpmPackage(Options options, LinuxRpmSystemEnvironment sysEnv) {
final var superPkgBuilder = createLinuxPackageBuilder(options, LINUX_RPM);
final var superPkgBuilder = createLinuxPackageBuilder(options, sysEnv, LINUX_RPM);
final var pkgBuilder = new LinuxRpmPackageBuilder(superPkgBuilder);
@ -81,9 +81,9 @@ final class LinuxFromOptions {
return pkgBuilder.create();
}
static LinuxDebPackage createLinuxDebPackage(Options options) {
static LinuxDebPackage createLinuxDebPackage(Options options, LinuxDebSystemEnvironment sysEnv) {
final var superPkgBuilder = createLinuxPackageBuilder(options, LINUX_DEB);
final var superPkgBuilder = createLinuxPackageBuilder(options, sysEnv, LINUX_DEB);
final var pkgBuilder = new LinuxDebPackageBuilder(superPkgBuilder);
@ -99,7 +99,7 @@ final class LinuxFromOptions {
return pkg;
}
private static LinuxPackageBuilder createLinuxPackageBuilder(Options options, StandardPackageType type) {
private static LinuxPackageBuilder createLinuxPackageBuilder(Options options, LinuxSystemEnvironment sysEnv, StandardPackageType type) {
final var app = createLinuxApplication(options);
@ -107,6 +107,8 @@ final class LinuxFromOptions {
final var pkgBuilder = new LinuxPackageBuilder(superPkgBuilder);
pkgBuilder.arch(sysEnv.packageArch());
LINUX_PACKAGE_DEPENDENCIES.ifPresentIn(options, pkgBuilder::additionalDependencies);
LINUX_APP_CATEGORY.ifPresentIn(options, pkgBuilder::category);
LINUX_MENU_GROUP.ifPresentIn(options, pkgBuilder::menuGroupName);

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2022, 2026, 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
@ -32,6 +32,7 @@ import java.util.List;
import java.util.Map;
import jdk.jpackage.internal.model.Launcher;
import jdk.jpackage.internal.model.Package;
import jdk.jpackage.internal.util.Enquoter;
/**
* Helper to install launchers as services using "systemd".

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -25,18 +25,20 @@
package jdk.jpackage.internal;
import java.io.IOException;
import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier;
import java.util.ArrayList;
import jdk.jpackage.internal.model.StandardPackageType;
import jdk.jpackage.internal.util.CommandOutputControl;
import jdk.jpackage.internal.util.Result;
final class LinuxPackageArch {
record LinuxPackageArch(String value) {
static String getValue(StandardPackageType pkgType) {
static Result<LinuxPackageArch> create(StandardPackageType pkgType) {
switch (pkgType) {
case LINUX_RPM -> {
return RpmPackageArch.VALUE;
return rpm().map(LinuxPackageArch::new);
}
case LINUX_DEB -> {
return DebPackageArch.VALUE;
return deb().map(LinuxPackageArch::new);
}
default -> {
throw new IllegalArgumentException();
@ -44,62 +46,51 @@ final class LinuxPackageArch {
}
}
private static class DebPackageArch {
static final String VALUE = toSupplier(DebPackageArch::getValue).get();
private static String getValue() throws IOException {
return Executor.of("dpkg", "--print-architecture").saveOutput(true)
.executeExpectSuccess().getOutput().get(0);
}
private static Result<String> deb() {
var exec = Executor.of("dpkg", "--print-architecture").saveOutput(true);
return Result.of(exec::executeExpectSuccess, IOException.class)
.flatMap(LinuxPackageArch::getStdoutFirstLine);
}
private static class RpmPackageArch {
/*
* Various ways to get rpm arch. Needed to address JDK-8233143. rpmbuild is mandatory for
* rpm packaging, try it first. rpm is optional and may not be available, use as the last
* resort.
*/
private static enum RpmArchReader {
Rpmbuild("rpmbuild", "--eval=%{_target_cpu}"),
Rpm("rpm", "--eval=%{_target_cpu}");
RpmArchReader(String... cmdline) {
this.cmdline = cmdline;
private static Result<String> rpm() {
var errors = new ArrayList<Exception>();
for (var tool : RpmArchReader.values()) {
var result = tool.getRpmArch();
if (result.hasValue()) {
return result;
} else {
errors.addAll(result.errors());
}
String getRpmArch() throws IOException {
Executor exec = Executor.of(cmdline).saveOutput(true);
switch (this) {
case Rpm -> {
exec.executeExpectSuccess();
}
case Rpmbuild -> {
if (exec.execute() != 0) {
return null;
}
}
default -> {
throw new UnsupportedOperationException();
}
}
return exec.getOutput().get(0);
}
private final String[] cmdline;
}
static final String VALUE = toSupplier(RpmPackageArch::getValue).get();
return Result.ofErrors(errors);
}
private static String getValue() throws IOException {
for (var rpmArchReader : RpmArchReader.values()) {
var rpmArchStr = rpmArchReader.getRpmArch();
if (rpmArchStr != null) {
return rpmArchStr;
}
}
throw new RuntimeException("error.rpm-arch-not-detected");
/*
* Various ways to get rpm arch. Needed to address JDK-8233143. rpmbuild is mandatory for
* rpm packaging, try it first. rpm is optional and may not be available, use as the last
* resort.
*/
private enum RpmArchReader {
RPMBUILD("rpmbuild", "--eval=%{_target_cpu}"),
RPM("rpm", "--eval=%{_target_cpu}");
RpmArchReader(String... cmdline) {
this.cmdline = cmdline;
}
Result<String> getRpmArch() {
var exec = Executor.of(cmdline).saveOutput(true);
return Result.of(exec::executeExpectSuccess, IOException.class)
.flatMap(LinuxPackageArch::getStdoutFirstLine);
}
private final String[] cmdline;
}
private static Result<String> getStdoutFirstLine(CommandOutputControl.Result result) {
return Result.of(() -> {
return result.stdout().stream().findFirst().orElseThrow(result::unexpected);
}, IOException.class);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -83,7 +83,7 @@ final class LinuxPackageBuilder {
category(),
Optional.ofNullable(additionalDependencies),
release(),
pkg.asStandardPackageType().map(LinuxPackageArch::getValue).orElseThrow()));
arch.value()));
}
LinuxPackageBuilder literalName(String v) {
@ -119,6 +119,11 @@ final class LinuxPackageBuilder {
return Optional.ofNullable(release);
}
LinuxPackageBuilder arch(LinuxPackageArch v) {
arch = v;
return this;
}
private static LinuxApplicationLayout usrTreePackageLayout(Path prefix, String packageName) {
final var lib = prefix.resolve(Path.of("lib", packageName));
return LinuxApplicationLayout.create(
@ -184,6 +189,7 @@ final class LinuxPackageBuilder {
private String category;
private String additionalDependencies;
private String release;
private LinuxPackageArch arch;
private final PackageBuilder pkgBuilder;

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -28,7 +28,7 @@ import static jdk.jpackage.internal.LinuxSystemEnvironment.mixin;
import jdk.jpackage.internal.util.Result;
public interface LinuxRpmSystemEnvironment extends LinuxSystemEnvironment, LinuxRpmSystemEnvironmentMixin {
interface LinuxRpmSystemEnvironment extends LinuxSystemEnvironment, LinuxRpmSystemEnvironmentMixin {
static Result<LinuxRpmSystemEnvironment> create(Result<LinuxSystemEnvironment> base) {
return mixin(LinuxRpmSystemEnvironment.class, base, LinuxRpmSystemEnvironmentMixin::create);

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -32,7 +32,7 @@ import java.util.stream.Stream;
import jdk.jpackage.internal.model.DottedVersion;
import jdk.jpackage.internal.util.Result;
public interface LinuxRpmSystemEnvironmentMixin {
interface LinuxRpmSystemEnvironmentMixin {
Path rpm();
Path rpmbuild();

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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,7 +27,6 @@ package jdk.jpackage.internal;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import jdk.jpackage.internal.model.PackageType;
@ -35,9 +34,10 @@ import jdk.jpackage.internal.model.StandardPackageType;
import jdk.jpackage.internal.util.CompositeProxy;
import jdk.jpackage.internal.util.Result;
public interface LinuxSystemEnvironment extends SystemEnvironment {
interface LinuxSystemEnvironment extends SystemEnvironment {
boolean soLookupAvailable();
PackageType nativePackageType();
LinuxPackageArch packageArch();
static Result<LinuxSystemEnvironment> create() {
return detectNativePackageType().map(LinuxSystemEnvironment::create).orElseGet(() -> {
@ -45,7 +45,7 @@ public interface LinuxSystemEnvironment extends SystemEnvironment {
});
}
static Optional<PackageType> detectNativePackageType() {
static Optional<StandardPackageType> detectNativePackageType() {
if (Internal.isDebian()) {
return Optional.of(StandardPackageType.LINUX_DEB);
} else if (Internal.isRpm()) {
@ -55,13 +55,14 @@ public interface LinuxSystemEnvironment extends SystemEnvironment {
}
}
static Result<LinuxSystemEnvironment> create(PackageType nativePackageType) {
return Result.ofValue(new Stub(LibProvidersLookup.supported(),
Objects.requireNonNull(nativePackageType)));
static Result<LinuxSystemEnvironment> create(StandardPackageType nativePackageType) {
return LinuxPackageArch.create(nativePackageType).map(arch -> {
return new Stub(LibProvidersLookup.supported(), nativePackageType, arch);
});
}
static <T, U extends LinuxSystemEnvironment> U createWithMixin(Class<U> type, LinuxSystemEnvironment base, T mixin) {
return CompositeProxy.create(type, base, mixin);
return CompositeProxy.build().invokeTunnel(CompositeProxyTunnel.INSTANCE).create(type, base, mixin);
}
static <T, U extends LinuxSystemEnvironment> Result<U> mixin(Class<U> type,
@ -79,7 +80,7 @@ public interface LinuxSystemEnvironment extends SystemEnvironment {
}
}
record Stub(boolean soLookupAvailable, PackageType nativePackageType) implements LinuxSystemEnvironment {
record Stub(boolean soLookupAvailable, PackageType nativePackageType, LinuxPackageArch packageArch) implements LinuxSystemEnvironment {
}
static final class Internal {

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -48,6 +48,7 @@ import jdk.jpackage.internal.model.Launcher;
import jdk.jpackage.internal.model.MacApplication;
import jdk.jpackage.internal.model.RuntimeLayout;
import jdk.jpackage.internal.util.PathUtils;
import jdk.jpackage.internal.util.Result;
import jdk.jpackage.internal.util.function.ExceptionBox;
@ -188,11 +189,9 @@ final class AppImageSigner {
}
private static boolean isXcodeDevToolsInstalled() {
try {
return Executor.of("/usr/bin/xcrun", "--help").setQuiet(true).execute() == 0;
} catch (IOException ex) {
return false;
}
return Result.of(
Executor.of("/usr/bin/xcrun", "--help").setQuiet(true)::executeExpectSuccess,
IOException.class).hasValue();
}
private static void unsign(Path path) throws IOException {

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -34,7 +34,6 @@ import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Stream;
public final class Codesign {
@ -94,14 +93,12 @@ public final class Codesign {
public void applyTo(Path path) throws IOException, CodesignException {
var exec = Executor.of(Stream.concat(
cmdline.stream(),
Stream.of(path.toString())).toArray(String[]::new)
).saveOutput(true);
var exec = Executor.of(cmdline).args(path.toString()).saveOutput(true);
configureExecutor.ifPresent(configure -> configure.accept(exec));
if (exec.execute() != 0) {
throw new CodesignException(exec.getOutput().toArray(String[]::new));
var result = exec.execute();
if (result.getExitCode() != 0) {
throw new CodesignException(result.getOutput().toArray(String[]::new));
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -36,7 +36,6 @@ import java.util.Optional;
import jdk.jpackage.internal.cli.Options;
import jdk.jpackage.internal.model.MacPackage;
import jdk.jpackage.internal.model.Package;
import jdk.jpackage.internal.util.Result;
public class MacBundlingEnvironment extends DefaultBundlingEnvironment {
@ -45,7 +44,7 @@ public class MacBundlingEnvironment extends DefaultBundlingEnvironment {
.defaultOperation(CREATE_MAC_DMG)
.bundler(SIGN_MAC_APP_IMAGE, MacBundlingEnvironment::signAppImage)
.bundler(CREATE_MAC_APP_IMAGE, MacBundlingEnvironment::createAppImage)
.bundler(CREATE_MAC_DMG, LazyLoad::dmgSysEnv, MacBundlingEnvironment::createDmdPackage)
.bundler(CREATE_MAC_DMG, MacDmgSystemEnvironment::create, MacBundlingEnvironment::createDmdPackage)
.bundler(CREATE_MAC_PKG, MacBundlingEnvironment::createPkgPackage));
}
@ -98,13 +97,4 @@ public class MacBundlingEnvironment extends DefaultBundlingEnvironment {
.predefinedAppImageLayout(APPLICATION_LAYOUT)
.predefinedRuntimeImageLayout(MacPackage::guessRuntimeLayout);
}
private static final class LazyLoad {
static Result<MacDmgSystemEnvironment> dmgSysEnv() {
return DMG_SYS_ENV;
}
private static final Result<MacDmgSystemEnvironment> DMG_SYS_ENV = MacDmgSystemEnvironment.create();
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -53,7 +53,7 @@ public final class MacCertificateUtils {
keychain.map(Keychain::asCliArg).ifPresent(args::add);
return toSupplier(() -> {
final var output = Executor.of(args.toArray(String[]::new))
final var output = Executor.of(args)
.setQuiet(true).saveOutput(true).executeExpectSuccess()
.getOutput();

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -33,11 +33,15 @@ import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import jdk.jpackage.internal.PackagingPipeline.PackageTaskID;
import jdk.jpackage.internal.PackagingPipeline.TaskID;
import jdk.jpackage.internal.model.MacDmgPackage;
@ -105,6 +109,10 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir,
return env.configDir().resolve(pkg.app().name() + "-license.plist");
}
private Path finalDmg() {
return outputDir.resolve(pkg.packageFileNameWithSuffix());
}
Path protoDmg() {
return dmgWorkdir().resolve("proto.dmg");
}
@ -128,6 +136,10 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir,
}
}
private Executor hdiutil(String... args) {
return Executor.of(sysEnv.hdiutil().toString()).args(args).storeOutputInFiles();
}
private void prepareDMGSetupScript() throws IOException {
Path dmgSetup = volumeScript();
Log.verbose(MessageFormat.format(
@ -211,13 +223,17 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir,
}
}
private String hdiUtilVerbosityFlag() {
return env.verbose() ? "-verbose" : "-quiet";
}
private void buildDMG() throws IOException {
boolean copyAppImage = false;
Path protoDMG = protoDmg();
Path finalDMG = outputDir.resolve(pkg.packageFileNameWithSuffix());
final Path protoDMG = protoDmg();
final Path finalDMG = finalDmg();
Path srcFolder = env.appImageDir();
final Path srcFolder = env.appImageDir();
Log.verbose(MessageFormat.format(I18N.getString(
"message.creating-dmg-file"), finalDMG.toAbsolutePath()));
@ -233,21 +249,17 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir,
Files.createDirectories(protoDMG.getParent());
Files.createDirectories(finalDMG.getParent());
String hdiUtilVerbosityFlag = env.verbose() ?
"-verbose" : "-quiet";
final String hdiUtilVerbosityFlag = hdiUtilVerbosityFlag();
// create temp image
ProcessBuilder pb = new ProcessBuilder(
sysEnv.hdiutil().toString(),
"create",
hdiUtilVerbosityFlag,
"-srcfolder", normalizedAbsolutePathString(srcFolder),
"-volname", volumeName(),
"-ov", normalizedAbsolutePathString(protoDMG),
"-fs", "HFS+",
"-format", "UDRW");
try {
IOUtils.exec(pb, false, null, true, Executor.INFINITE_TIMEOUT);
hdiutil("create",
hdiUtilVerbosityFlag,
"-srcfolder", normalizedAbsolutePathString(srcFolder),
"-volname", volumeName(),
"-ov", normalizedAbsolutePathString(protoDMG),
"-fs", "HFS+",
"-format", "UDRW").executeExpectSuccess();
} catch (IOException ex) {
Log.verbose(ex); // Log exception
@ -260,31 +272,26 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir,
// not be bigger, but it will able to hold additional 50 megabytes of data.
// We need extra room for icons and background image. When we providing
// actual files to hdiutil, it will create DMG with ~50 megabytes extra room.
pb = new ProcessBuilder(
sysEnv.hdiutil().toString(),
"create",
hdiUtilVerbosityFlag,
"-size", String.valueOf(size),
"-volname", volumeName(),
"-ov", normalizedAbsolutePathString(protoDMG),
"-fs", "HFS+");
new RetryExecutor()
.setMaxAttemptsCount(10)
.setAttemptTimeoutMillis(3000)
.setWriteOutputToFile(true)
.execute(pb);
hdiutil(
"create",
hdiUtilVerbosityFlag,
"-size", String.valueOf(size),
"-volname", volumeName(),
"-ov", normalizedAbsolutePathString(protoDMG),
"-fs", "HFS+"
).retry()
.setMaxAttemptsCount(10)
.setAttemptTimeout(3, TimeUnit.SECONDS)
.execute();
}
final Path mountedVolume = volumePath();
// mount temp image
pb = new ProcessBuilder(
sysEnv.hdiutil().toString(),
"attach",
hdiutil("attach",
normalizedAbsolutePathString(protoDMG),
hdiUtilVerbosityFlag,
"-mountroot", protoDMG.getParent().toString());
IOUtils.exec(pb, false, null, true, Executor.INFINITE_TIMEOUT);
final Path mountedVolume = volumePath();
"-mountroot", mountedVolume.getParent().toString()).executeExpectSuccess();
// Copy app image, since we did not create DMG with it, but instead we created
// empty one.
@ -302,9 +309,13 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir,
// to install-dir in DMG as critical error, since it can fail in
// headless environment.
try {
pb = new ProcessBuilder(sysEnv.osascript().toString(),
normalizedAbsolutePathString(volumeScript()));
IOUtils.exec(pb, 180); // Wait 3 minutes. See JDK-8248248.
Executor.of(
sysEnv.osascript().toString(),
normalizedAbsolutePathString(volumeScript())
)
// Wait 3 minutes. See JDK-8248248.
.timeout(3, TimeUnit.MINUTES)
.executeExpectSuccess();
} catch (IOException ex) {
Log.verbose(ex);
}
@ -325,18 +336,18 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir,
// but it seems Finder excepts these bytes to be
// "icnC" for the volume icon
// (might not work on Mac 10.13 with old XCode)
pb = new ProcessBuilder(
Executor.of(
sysEnv.setFileUtility().orElseThrow().toString(),
"-c", "icnC",
normalizedAbsolutePathString(volumeIconFile));
IOUtils.exec(pb);
normalizedAbsolutePathString(volumeIconFile)
).executeExpectSuccess();
volumeIconFile.toFile().setReadOnly();
pb = new ProcessBuilder(
Executor.of(
sysEnv.setFileUtility().orElseThrow().toString(),
"-a", "C",
normalizedAbsolutePathString(mountedVolume));
IOUtils.exec(pb);
normalizedAbsolutePathString(mountedVolume)
).executeExpectSuccess();
} catch (IOException ex) {
Log.error(ex.getMessage());
Log.verbose("Cannot enable custom icon using SetFile utility");
@ -347,85 +358,23 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir,
} finally {
// Detach the temporary image
pb = new ProcessBuilder(
sysEnv.hdiutil().toString(),
"detach",
hdiUtilVerbosityFlag,
normalizedAbsolutePathString(mountedVolume));
// "hdiutil detach" might not work right away due to resource busy error, so
// repeat detach several times.
RetryExecutor retryExecutor = new RetryExecutor();
// Image can get detach even if we got resource busy error, so stop
// trying to detach it if it is no longer attached.
retryExecutor.setExecutorInitializer(exec -> {
if (!Files.exists(mountedVolume)) {
retryExecutor.abort();
}
});
try {
// 10 times with 6 second delays.
retryExecutor.setMaxAttemptsCount(10).setAttemptTimeoutMillis(6000)
.execute(pb);
} catch (IOException ex) {
if (!retryExecutor.isAborted()) {
// Now force to detach if it still attached
if (Files.exists(mountedVolume)) {
pb = new ProcessBuilder(
sysEnv.hdiutil().toString(),
"detach",
"-force",
hdiUtilVerbosityFlag,
normalizedAbsolutePathString(mountedVolume));
IOUtils.exec(pb, false, null, true, Executor.INFINITE_TIMEOUT);
}
}
}
detachVolume();
}
// Compress it to a new image
pb = new ProcessBuilder(
sysEnv.hdiutil().toString(),
"convert",
normalizedAbsolutePathString(protoDMG),
hdiUtilVerbosityFlag,
"-format", "UDZO",
"-o", normalizedAbsolutePathString(finalDMG));
try {
new RetryExecutor()
.setMaxAttemptsCount(10)
.setAttemptTimeoutMillis(3000)
.execute(pb);
} catch (Exception ex) {
// Convert might failed if something holds file. Try to convert copy.
Path protoCopyDMG = protoCopyDmg();
Files.copy(protoDMG, protoCopyDMG);
try {
pb = new ProcessBuilder(
sysEnv.hdiutil().toString(),
"convert",
normalizedAbsolutePathString(protoCopyDMG),
hdiUtilVerbosityFlag,
"-format", "UDZO",
"-o", normalizedAbsolutePathString(finalDMG));
IOUtils.exec(pb, false, null, true, Executor.INFINITE_TIMEOUT);
} finally {
Files.deleteIfExists(protoCopyDMG);
}
}
convertProtoDmg();
//add license if needed
if (pkg.licenseFile().isPresent()) {
pb = new ProcessBuilder(
sysEnv.hdiutil().toString(),
hdiutil(
"udifrez",
normalizedAbsolutePathString(finalDMG),
"-xml",
normalizedAbsolutePathString(licenseFile())
);
new RetryExecutor()
.setMaxAttemptsCount(10)
.setAttemptTimeoutMillis(3000)
.execute(pb);
).retry()
.setMaxAttemptsCount(10)
.setAttemptTimeout(3, TimeUnit.SECONDS)
.execute();
}
try {
@ -441,6 +390,69 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir,
}
private void detachVolume() throws IOException {
var mountedVolume = volumePath();
// "hdiutil detach" might not work right away due to resource busy error, so
// repeat detach several times.
Globals.instance().objectFactory().<Void, IOException>retryExecutor(IOException.class).setExecutable(context -> {
List<String> cmdline = new ArrayList<>();
cmdline.add("detach");
if (context.isLastAttempt()) {
// The last attempt, force detach.
cmdline.add("-force");
}
cmdline.addAll(List.of(
hdiUtilVerbosityFlag(),
normalizedAbsolutePathString(mountedVolume)
));
// The image can get detached even if we get a resource busy error,
// so execute the detach command without checking the exit code.
var result = hdiutil(cmdline.toArray(String[]::new)).execute();
if (result.getExitCode() == 0 || !Files.exists(mountedVolume)) {
// Detached successfully!
return null;
} else {
throw result.unexpected();
}
}).setMaxAttemptsCount(10).setAttemptTimeout(6, TimeUnit.SECONDS).execute();
}
private void convertProtoDmg() throws IOException {
Function<Path, Executor> convert = srcDmg -> {
return hdiutil(
"convert",
normalizedAbsolutePathString(srcDmg),
hdiUtilVerbosityFlag(),
"-format", "UDZO",
"-o", normalizedAbsolutePathString(finalDmg()));
};
// Convert it to a new image.
try {
convert.apply(protoDmg()).retry()
.setMaxAttemptsCount(10)
.setAttemptTimeout(3, TimeUnit.SECONDS)
.execute();
} catch (IOException ex) {
Log.verbose(ex);
// Something holds the file, try to convert a copy.
Path copyDmg = protoCopyDmg();
Files.copy(protoDmg(), copyDmg);
try {
convert.apply(copyDmg).executeExpectSuccess();
} finally {
Files.deleteIfExists(copyDmg);
}
}
}
// Background image name in resources
private static final String DEFAULT_BACKGROUND_IMAGE = "background_dmg.tiff";
private static final String DEFAULT_DMG_SETUP_SCRIPT = "DMGsetup.scpt";

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -25,10 +25,11 @@
package jdk.jpackage.internal;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jdk.jpackage.internal.util.Result;
@ -54,41 +55,31 @@ record MacDmgSystemEnvironment(Path hdiutil, Path osascript, Optional<Path> setF
// Location of SetFile utility may be different depending on MacOS version
// We look for several known places and if none of them work will
// try to find it
private static Optional<Path> findSetFileUtility() {
String typicalPaths[] = {"/Developer/Tools/SetFile",
"/usr/bin/SetFile", "/Developer/usr/bin/SetFile"};
static Optional<Path> findSetFileUtility() {
return SETFILE_KNOWN_PATHS.stream().filter(setFilePath -> {
// Validate SetFile, if Xcode is not installed it will run, but exit with error code
return Result.of(
Executor.of(setFilePath.toString(), "-h").setQuiet(true)::executeExpectSuccess,
IOException.class).hasValue();
}).findFirst().or(() -> {
// generic find attempt
final var executor = Executor.of("/usr/bin/xcrun", "-find", "SetFile").setQuiet(true).saveFirstLineOfOutput();
final var setFilePath = Stream.of(typicalPaths).map(Path::of).filter(Files::isExecutable).findFirst();
if (setFilePath.isPresent()) {
// Validate SetFile, if Xcode is not installed it will run, but exit with error
// code
try {
if (Executor.of(setFilePath.orElseThrow().toString(), "-h").setQuiet(true).execute() == 0) {
return setFilePath;
}
} catch (Exception ignored) {
// No need for generic find attempt. We found it, but it does not work.
// Probably due to missing xcode.
return Optional.empty();
}
}
// generic find attempt
try {
final var executor = Executor.of("/usr/bin/xcrun", "-find", "SetFile");
final var code = executor.setQuiet(true).saveOutput(true).execute();
if (code == 0 && !executor.getOutput().isEmpty()) {
final var firstLine = executor.getOutput().getFirst();
Path f = Path.of(firstLine);
if (new ToolValidator(f).checkExistsOnly().validate() == null) {
return Optional.of(f.toAbsolutePath());
}
}
} catch (IOException ignored) {}
return Optional.empty();
return Result.of(executor::executeExpectSuccess, IOException.class).flatMap(execResult -> {
return Result.of(() -> {
return execResult.stdout().stream().findFirst().map(Path::of).orElseThrow(execResult::unexpected);
}, Exception.class);
}).value().filter(v -> {
return new ToolValidator(v).checkExistsOnly().validate() == null;
}).map(Path::toAbsolutePath);
});
}
static final List<Path> SETFILE_KNOWN_PATHS = Stream.of(
"/Developer/Tools/SetFile",
"/usr/bin/SetFile",
"/Developer/usr/bin/SetFile").map(Path::of).collect(Collectors.toUnmodifiableList());
private static final Path HDIUTIL = Path.of("/usr/bin/hdiutil");
private static final Path OSASCRIPT = Path.of("/usr/bin/osascript");
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -32,7 +32,6 @@ import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.MessageFormat;
@ -57,6 +56,7 @@ import jdk.jpackage.internal.PackagingPipeline.PackageTaskID;
import jdk.jpackage.internal.PackagingPipeline.TaskID;
import jdk.jpackage.internal.model.MacPkgPackage;
import jdk.jpackage.internal.resources.ResourceLocator;
import jdk.jpackage.internal.util.Enquoter;
import jdk.jpackage.internal.util.XmlUtils;
import org.xml.sax.SAXException;
@ -108,7 +108,7 @@ record MacPkgPackager(BuildEnv env, MacPkgPackage pkg, Optional<Services> servic
cmdline.addAll(allPkgbuildArgs());
try {
Files.createDirectories(path.getParent());
IOUtils.exec(new ProcessBuilder(cmdline), false, null, true, Executor.INFINITE_TIMEOUT);
Executor.of(cmdline).executeExpectSuccess();
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
@ -487,15 +487,13 @@ record MacPkgPackager(BuildEnv env, MacPkgPackage pkg, Optional<Services> servic
Files.createDirectories(cpl.getParent());
final var pb = new ProcessBuilder("/usr/bin/pkgbuild",
Executor.of("/usr/bin/pkgbuild",
"--root",
normalizedAbsolutePathString(env.appImageDir()),
"--install-location",
normalizedAbsolutePathString(installLocation()),
"--analyze",
normalizedAbsolutePathString(cpl));
IOUtils.exec(pb, false, null, true, Executor.INFINITE_TIMEOUT);
normalizedAbsolutePathString(cpl)).executeExpectSuccess();
patchCPLFile(cpl);
}
@ -544,8 +542,7 @@ record MacPkgPackager(BuildEnv env, MacPkgPackage pkg, Optional<Services> servic
}
commandLine.add(normalizedAbsolutePathString(finalPkg));
final var pb = new ProcessBuilder(commandLine);
IOUtils.exec(pb, false, null, true, Executor.INFINITE_TIMEOUT);
Executor.of(commandLine).executeExpectSuccess();
}
private static Optional<Services> createServices(BuildEnv env, MacPkgPackage pkg) {

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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,15 +27,17 @@ package jdk.jpackage.internal;
import java.io.Closeable;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import jdk.internal.util.OSVersion;
import jdk.jpackage.internal.util.function.ThrowingConsumer;
final class TempKeychain implements Closeable {
static void withKeychains(ThrowingConsumer<List<Keychain>, ? extends Exception> keychainConsumer, List<Keychain> keychains) throws Exception {
static void withKeychains(Consumer<List<Keychain>> keychainConsumer, List<Keychain> keychains) {
keychains.forEach(Objects::requireNonNull);
if (keychains.isEmpty() || OSVersion.current().compareTo(new OSVersion(10, 12)) < 0) {
keychainConsumer.accept(keychains);
@ -43,11 +45,14 @@ final class TempKeychain implements Closeable {
// we need this for OS X 10.12+
try (var tempKeychain = new TempKeychain(keychains)) {
keychainConsumer.accept(tempKeychain.keychains);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
}
static void withKeychain(ThrowingConsumer<Keychain, ? extends Exception> keychainConsumer, Keychain keychain) throws Exception {
static void withKeychain(Consumer<Keychain> keychainConsumer, Keychain keychain) {
Objects.requireNonNull(keychainConsumer);
withKeychains(keychains -> {
keychainConsumer.accept(keychains.getFirst());
@ -78,7 +83,7 @@ final class TempKeychain implements Closeable {
args.addAll(missingKeychains.stream().map(Keychain::asCliArg).toList());
Executor.of(args.toArray(String[]::new)).executeExpectSuccess();
Executor.of(args).executeExpectSuccess();
}
}
@ -89,7 +94,7 @@ final class TempKeychain implements Closeable {
@Override
public void close() throws IOException {
if (!restoreKeychainsCmd.isEmpty()) {
Executor.of(restoreKeychainsCmd.toArray(String[]::new)).executeExpectSuccess();
Executor.of(restoreKeychainsCmd).executeExpectSuccess();
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -65,10 +65,10 @@ class DefaultBundlingEnvironment implements CliBundlingEnvironment {
Map<BundlingOperationDescriptor, Supplier<Result<Consumer<Options>>>> bundlers) {
this.bundlers = bundlers.entrySet().stream().collect(toMap(Map.Entry::getKey, e -> {
return new CachingSupplier<>(e.getValue());
return runOnce(e.getValue());
}));
this.defaultOperationSupplier = Objects.requireNonNull(defaultOperationSupplier).map(CachingSupplier::new);
this.defaultOperationSupplier = Objects.requireNonNull(defaultOperationSupplier).map(DefaultBundlingEnvironment::runOnce);
}
@ -98,6 +98,11 @@ class DefaultBundlingEnvironment implements CliBundlingEnvironment {
return bundler(op, () -> Result.ofValue(bundler));
}
Builder mutate(Consumer<Builder> mutator) {
mutator.accept(this);
return this;
}
private Supplier<Optional<BundlingOperationDescriptor>> defaultOperationSupplier;
private final Map<BundlingOperationDescriptor, Supplier<Result<Consumer<Options>>>> bundlers = new HashMap<>();
}
@ -107,6 +112,10 @@ class DefaultBundlingEnvironment implements CliBundlingEnvironment {
return new Builder();
}
static <T> Supplier<T> runOnce(Supplier<T> supplier) {
return new CachingSupplier<>(supplier);
}
static <T extends SystemEnvironment> Supplier<Result<Consumer<Options>>> createBundlerSupplier(
Supplier<Result<T>> sysEnvResultSupplier, BiConsumer<Options, T> bundler) {
Objects.requireNonNull(sysEnvResultSupplier);
@ -279,5 +288,5 @@ class DefaultBundlingEnvironment implements CliBundlingEnvironment {
private final Map<BundlingOperationDescriptor, Supplier<Result<Consumer<Options>>>> bundlers;
private final Optional<CachingSupplier<Optional<BundlingOperationDescriptor>>> defaultOperationSupplier;
private final Optional<Supplier<Optional<BundlingOperationDescriptor>>> defaultOperationSupplier;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2026, 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
@ -25,53 +25,153 @@
package jdk.jpackage.internal;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.io.PrintStream;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import java.util.spi.ToolProvider;
import java.util.stream.Stream;
import jdk.jpackage.internal.util.CommandLineFormat;
import jdk.jpackage.internal.util.CommandOutputControl;
import jdk.jpackage.internal.util.CommandOutputControl.ProcessAttributes;
import jdk.jpackage.internal.util.CommandOutputControl.Result;
import jdk.jpackage.internal.util.RetryExecutor;
import jdk.jpackage.internal.util.function.ExceptionBox;
public final class Executor {
final class Executor {
Executor() {
static Executor of(String... cmdline) {
return of(List.of(cmdline));
}
Executor setOutputConsumer(Consumer<Stream<String>> v) {
outputConsumer = v;
return this;
static Executor of(List<String> cmdline) {
return of(new ProcessBuilder(cmdline));
}
static Executor of(ProcessBuilder pb) {
return Globals.instance().objectFactory().executor().processBuilder(pb);
}
public Executor() {
commandOutputControl = new CommandOutputControl();
args = new ArrayList<>();
}
private Executor(Executor other) {
commandOutputControl = other.commandOutputControl.copy();
quietCommand = other.quietCommand;
args = new ArrayList<>(other.args);
processBuilder = other.processBuilder;
toolProvider = other.toolProvider;
timeout = other.timeout;
mapper = other.mapper;
}
Executor saveOutput(boolean v) {
saveOutput = v;
commandOutputControl.saveOutput(v);
return this;
}
Executor setWriteOutputToFile(boolean v) {
writeOutputToFile = v;
Executor saveOutput() {
return saveOutput(true);
}
Executor saveFirstLineOfOutput() {
commandOutputControl.saveFirstLineOfOutput();
return this;
}
Executor setTimeout(long v) {
Executor charset(Charset v) {
commandOutputControl.charset(v);
return this;
}
Executor storeOutputInFiles(boolean v) {
commandOutputControl.storeOutputInFiles(v);
return this;
}
Executor storeOutputInFiles() {
return storeOutputInFiles(true);
}
Executor binaryOutput(boolean v) {
commandOutputControl.binaryOutput(v);
return this;
}
Executor binaryOutput() {
return binaryOutput(true);
}
Executor discardStdout(boolean v) {
commandOutputControl.discardStdout(v);
return this;
}
Executor discardStdout() {
return discardStdout(true);
}
Executor discardStderr(boolean v) {
commandOutputControl.discardStderr(v);
return this;
}
Executor discardStderr() {
return discardStderr(true);
}
Executor timeout(long v, TimeUnit unit) {
return timeout(Duration.of(v, unit.toChronoUnit()));
}
Executor timeout(Duration v) {
timeout = v;
if (timeout != INFINITE_TIMEOUT) {
// Redirect output to file if timeout is requested, otherwise we will
// reading until process ends and timeout will never be reached.
setWriteOutputToFile(true);
}
return this;
}
Executor setProcessBuilder(ProcessBuilder v) {
pb = v;
Executor toolProvider(ToolProvider v) {
toolProvider = Objects.requireNonNull(v);
processBuilder = null;
return this;
}
Executor setCommandLine(String... cmdline) {
return setProcessBuilder(new ProcessBuilder(cmdline));
Optional<ToolProvider> toolProvider() {
return Optional.ofNullable(toolProvider);
}
Executor processBuilder(ProcessBuilder v) {
processBuilder = Objects.requireNonNull(v);
toolProvider = null;
return this;
}
Optional<ProcessBuilder> processBuilder() {
return Optional.ofNullable(processBuilder);
}
Executor args(List<String> v) {
args.addAll(v);
return this;
}
Executor args(String... args) {
return args(List.of(args));
}
List<String> args() {
return args;
}
Executor setQuiet(boolean v) {
@ -79,159 +179,207 @@ public final class Executor {
return this;
}
List<String> getOutput() {
return output;
}
Executor executeExpectSuccess() throws IOException {
int ret = execute();
if (0 != ret) {
throw new IOException(
String.format("Command %s exited with %d code",
createLogMessage(pb, false), ret));
}
Executor mapper(UnaryOperator<Executor> v) {
mapper = v;
return this;
}
int execute() throws IOException {
output = null;
Optional<UnaryOperator<Executor>> mapper() {
return Optional.ofNullable(mapper);
}
boolean needProcessOutput = outputConsumer != null || Log.isVerbose() || saveOutput;
Path outputFile = null;
if (needProcessOutput) {
pb.redirectErrorStream(true);
if (writeOutputToFile) {
outputFile = Files.createTempFile("jpackageOutputTempFile", ".tmp");
pb.redirectOutput(outputFile.toFile());
Executor copy() {
return new Executor(this);
}
Result execute() throws IOException {
if (mapper != null) {
var mappedExecutor = Objects.requireNonNull(mapper.apply(this));
if (mappedExecutor != this) {
return mappedExecutor.execute();
}
}
var coc = commandOutputControl.copy();
final CommandOutputControl.Executable exec;
if (processBuilder != null) {
exec = coc.createExecutable(copyProcessBuilder());
} else if (toolProvider != null) {
exec = coc.createExecutable(toolProvider, args.toArray(String[]::new));
} else {
// We are not going to read process output, so need to notify
// ProcessBuilder about this. Otherwise some processes might just
// hang up (`ldconfig -p`).
pb.redirectError(ProcessBuilder.Redirect.DISCARD);
pb.redirectOutput(ProcessBuilder.Redirect.DISCARD);
throw new IllegalStateException("No target to execute");
}
if (!quietCommand) {
Log.verbose(String.format("Running %s", createLogMessage(pb, true)));
PrintableOutputBuilder printableOutputBuilder;
if (dumpOutput()) {
printableOutputBuilder = new PrintableOutputBuilder(coc);
} else {
printableOutputBuilder = null;
}
Process p = pb.start();
int code = 0;
if (writeOutputToFile) {
try {
code = waitForProcess(p);
} catch (InterruptedException ex) {
Log.verbose(ex);
throw new RuntimeException(ex);
}
}
if (needProcessOutput) {
final List<String> savedOutput;
Supplier<Stream<String>> outputStream;
if (writeOutputToFile) {
output = savedOutput = Files.readAllLines(outputFile);
Files.delete(outputFile);
outputStream = () -> {
if (savedOutput != null) {
return savedOutput.stream();
}
return null;
};
if (outputConsumer != null) {
outputConsumer.accept(outputStream.get());
}
} else {
try (var br = new BufferedReader(new InputStreamReader(
p.getInputStream()))) {
if ((outputConsumer != null || Log.isVerbose())
|| saveOutput) {
savedOutput = br.lines().toList();
} else {
savedOutput = null;
}
output = savedOutput;
outputStream = () -> {
if (savedOutput != null) {
return savedOutput.stream();
}
return br.lines();
};
if (outputConsumer != null) {
outputConsumer.accept(outputStream.get());
}
if (savedOutput == null) {
// For some processes on Linux if the output stream
// of the process is opened but not consumed, the process
// would exit with code 141.
// It turned out that reading just a single line of process
// output fixes the problem, but let's process
// all of the output, just in case.
br.lines().forEach(x -> {});
}
}
}
if (dumpOutput()) {
Log.verbose(String.format("Running %s", CommandLineFormat.DEFAULT.apply(List.of(commandLine().getFirst()))));
}
Result result;
try {
if (!writeOutputToFile) {
code = p.waitFor();
}
if (!quietCommand) {
Log.verbose(pb.command(), getOutput(), code, IOUtils.getPID(p));
}
return code;
} catch (InterruptedException ex) {
Log.verbose(ex);
throw new RuntimeException(ex);
}
}
private int waitForProcess(Process p) throws InterruptedException {
if (timeout == INFINITE_TIMEOUT) {
return p.waitFor();
} else {
if (p.waitFor(timeout, TimeUnit.SECONDS)) {
return p.exitValue();
if (timeout == null) {
result = exec.execute();
} else {
Log.verbose(String.format("Command %s timeout after %d seconds",
createLogMessage(pb, false), timeout));
p.destroy();
return -1;
result = exec.execute(timeout.toMillis(), TimeUnit.MILLISECONDS);
}
} catch (InterruptedException ex) {
throw ExceptionBox.toUnchecked(ex);
}
if (dumpOutput()) {
log(result, printableOutputBuilder.create());
}
return result;
}
Result executeExpectSuccess() throws IOException {
return execute().expectExitCode(0);
}
Result executeExpect(int mainExitCode, int... otherExitCodes) throws IOException {
return execute().expectExitCode(mainExitCode, otherExitCodes);
}
RetryExecutor<Result, IOException> retry() {
return Globals.instance().objectFactory().<Result, IOException>retryExecutor(IOException.class)
.setExecutable(this::executeExpectSuccess);
}
RetryExecutor<Result, IOException> retryOnKnownErrorMessage(String msg) {
Objects.requireNonNull(msg);
return saveOutput().retry().setExecutable(() -> {
// Execute it without exit code check.
var result = execute();
if (result.stderr().stream().anyMatch(msg::equals)) {
throw result.unexpected();
}
return result;
});
}
List<String> commandLine() {
if (processBuilder != null) {
return Stream.of(processBuilder.command(), args).flatMap(Collection::stream).toList();
} else if (toolProvider != null) {
return Stream.concat(Stream.of(toolProvider.name()), args.stream()).toList();
} else {
throw new IllegalStateException("No target to execute");
}
}
private ProcessBuilder copyProcessBuilder() {
if (processBuilder == null) {
throw new IllegalStateException();
}
var copy = new ProcessBuilder(commandLine());
copy.directory(processBuilder.directory());
var env = copy.environment();
env.clear();
env.putAll(processBuilder.environment());
return copy;
}
private boolean dumpOutput() {
return Log.isVerbose() && !quietCommand;
}
private static void log(Result result, String printableOutput) throws IOException {
Objects.requireNonNull(result);
Objects.requireNonNull(printableOutput);
Optional<Long> pid;
if (result.execAttrs() instanceof ProcessAttributes attrs) {
pid = attrs.pid();
} else {
pid = Optional.empty();
}
var sb = new StringBuilder();
sb.append("Command");
pid.ifPresent(p -> {
sb.append(" [PID: ").append(p).append("]");
});
sb.append(":\n ").append(result.execAttrs());
Log.verbose(sb.toString());
if (!printableOutput.isEmpty()) {
sb.delete(0, sb.length());
sb.append("Output:");
try (var lines = new BufferedReader(new StringReader(printableOutput)).lines()) {
lines.forEach(line -> {
sb.append("\n ").append(line);
});
}
Log.verbose(sb.toString());
}
result.exitCode().ifPresentOrElse(exitCode -> {
Log.verbose("Returned: " + exitCode + "\n");
}, () -> {
Log.verbose("Aborted: timed-out" + "\n");
});
}
private static final class PrintableOutputBuilder {
PrintableOutputBuilder(CommandOutputControl coc) {
coc.dumpOutput(true);
charset = coc.charset();
if (coc.isBinaryOutput()) {
// Assume binary output goes into stdout and text error messages go into stderr, so keep them separated.
sinks = new ByteArrayOutputStream[2];
sinks[0] = new ByteArrayOutputStream();
sinks[1] = new ByteArrayOutputStream();
coc.dumpStdout(new PrintStream(sinks[0], false, charset))
.dumpStderr(new PrintStream(sinks[1], false, charset));
} else {
sinks = new ByteArrayOutputStream[1];
sinks[0] = new ByteArrayOutputStream();
var ps = new PrintStream(sinks[0], false, charset);
// Redirect stderr in stdout.
coc.dumpStdout(ps).dumpStderr(ps);
}
}
}
static Executor of(String... cmdline) {
return new Executor().setCommandLine(cmdline);
}
static Executor of(ProcessBuilder pb) {
return new Executor().setProcessBuilder(pb);
}
private static String createLogMessage(ProcessBuilder pb, boolean quiet) {
StringBuilder sb = new StringBuilder();
sb.append((quiet) ? pb.command().get(0) : pb.command());
if (pb.directory() != null) {
sb.append(String.format(" in %s", pb.directory().getAbsolutePath()));
String create() {
if (isBinaryOutput()) {
// In case of binary output:
// - Convert binary stdout to text using ISO-8859-1 encoding and
// replace non-printable characters with the question mark symbol (?).
// - Convert binary stderr to text using designated encoding (assume stderr is always a character stream).
// - Merge text stdout and stderr into a single string;
// stderr first, stdout follows, with the aim to present user error messages first.
var sb = new StringBuilder();
var stdout = sinks[0].toString(StandardCharsets.ISO_8859_1).replaceAll("[^\\p{Print}\\p{Space}]", "?");
return sb.append(sinks[1].toString(charset)).append(stdout).toString();
} else {
return sinks[0].toString(charset);
}
}
return sb.toString();
private boolean isBinaryOutput() {
return sinks.length == 2;
}
private final ByteArrayOutputStream sinks[];
private final Charset charset;
}
public static final int INFINITE_TIMEOUT = -1;
private ProcessBuilder pb;
private boolean saveOutput;
private boolean writeOutputToFile;
private final CommandOutputControl commandOutputControl;
private boolean quietCommand;
private long timeout = INFINITE_TIMEOUT;
private List<String> output;
private Consumer<Stream<String>> outputConsumer;
private final List<String> args;
private ProcessBuilder processBuilder;
private ToolProvider toolProvider;
private Duration timeout;
private UnaryOperator<Executor> mapper;
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal;
@FunctionalInterface
interface ExecutorFactory {
Executor executor();
static final ExecutorFactory DEFAULT = Executor::new;
}

View File

@ -0,0 +1,71 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal;
import java.util.Optional;
import java.util.function.Supplier;
public final class Globals {
private Globals() {
}
Globals objectFactory(ObjectFactory v) {
checkMutable();
objectFactory = Optional.ofNullable(v).orElse(ObjectFactory.DEFAULT);
return this;
}
ObjectFactory objectFactory() {
return objectFactory;
}
Globals executorFactory(ExecutorFactory v) {
return objectFactory(ObjectFactory.build(objectFactory).executorFactory(v).create());
}
public static int main(Supplier<Integer> mainBody) {
if (INSTANCE.isBound()) {
return mainBody.get();
} else {
return ScopedValue.where(INSTANCE, new Globals()).call(mainBody::get);
}
}
public static Globals instance() {
return INSTANCE.orElse(DEFAULT);
}
private void checkMutable() {
if (this == DEFAULT) {
throw new UnsupportedOperationException("Can't modify immutable instance");
}
}
private ObjectFactory objectFactory = ObjectFactory.DEFAULT;
private static final ScopedValue<Globals> INSTANCE = ScopedValue.newInstance();
private static final Globals DEFAULT = new Globals();
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2012, 2026, 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
@ -26,12 +26,9 @@
package jdk.jpackage.internal;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import jdk.jpackage.internal.model.JPackageException;
/**
@ -50,46 +47,6 @@ final class IOUtils {
StandardCopyOption.COPY_ATTRIBUTES);
}
public static void exec(ProcessBuilder pb)
throws IOException {
exec(pb, false, null, false, Executor.INFINITE_TIMEOUT);
}
// timeout in seconds. -1 will be return if process timeouts.
public static void exec(ProcessBuilder pb, long timeout)
throws IOException {
exec(pb, false, null, false, timeout);
}
static void exec(ProcessBuilder pb, boolean testForPresenceOnly,
PrintStream consumer, boolean writeOutputToFile, long timeout)
throws IOException {
exec(pb, testForPresenceOnly, consumer, writeOutputToFile,
timeout, false);
}
static void exec(ProcessBuilder pb, boolean testForPresenceOnly,
PrintStream consumer, boolean writeOutputToFile,
long timeout, boolean quiet) throws IOException {
List<String> output = new ArrayList<>();
Executor exec = Executor.of(pb)
.setWriteOutputToFile(writeOutputToFile)
.setTimeout(timeout)
.setQuiet(quiet)
.setOutputConsumer(lines -> {
lines.forEach(output::add);
if (consumer != null) {
output.forEach(consumer::println);
}
});
if (testForPresenceOnly) {
exec.execute();
} else {
exec.executeExpectSuccess();
}
}
static void writableOutputDir(Path outdir) {
if (!Files.isDirectory(outdir)) {
try {
@ -103,15 +60,4 @@ final class IOUtils {
throw new JPackageException(I18N.format("error.cannot-write-to-output-dir", outdir.toAbsolutePath()));
}
}
public static long getPID(Process p) {
try {
return p.pid();
} catch (UnsupportedOperationException ex) {
Log.verbose(ex); // Just log exception and ignore it. This method
// is used for verbose output, so not a problem
// if unsupported.
return -1;
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2015, 2026, 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,13 +22,14 @@
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal;
import static jdk.jpackage.internal.model.RuntimeBuilder.getDefaultModulePath;
import static jdk.jpackage.internal.util.function.ThrowingRunnable.toRunnable;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.module.Configuration;
import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleFinder;
@ -50,7 +51,6 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import jdk.internal.module.ModulePath;
import jdk.jpackage.internal.model.AppImageLayout;
import jdk.jpackage.internal.model.JPackageException;
import jdk.jpackage.internal.model.LauncherModularStartupInfo;
import jdk.jpackage.internal.model.LauncherStartupInfo;
import jdk.jpackage.internal.model.RuntimeBuilder;
@ -58,27 +58,15 @@ import jdk.jpackage.internal.model.RuntimeBuilder;
final class JLinkRuntimeBuilder implements RuntimeBuilder {
private JLinkRuntimeBuilder(List<String> jlinkCmdLine) {
this.jlinkCmdLine = jlinkCmdLine;
this.jlinkCmdLine = Objects.requireNonNull(jlinkCmdLine);
}
@Override
public void create(AppImageLayout appImageLayout) {
var args = new ArrayList<String>();
args.add("--output");
args.add(appImageLayout.runtimeDirectory().toString());
args.addAll(jlinkCmdLine);
StringWriter writer = new StringWriter();
PrintWriter pw = new PrintWriter(writer);
int retVal = LazyLoad.JLINK_TOOL.run(pw, pw, args.toArray(String[]::new));
String jlinkOut = writer.toString();
args.add(0, "jlink");
Log.verbose(args, List.of(jlinkOut), retVal, -1);
if (retVal != 0) {
throw new JPackageException(I18N.format("error.jlink.failed", jlinkOut));
}
toRunnable(Executor.of()
.toolProvider(LazyLoad.JLINK_TOOL)
.args("--output", appImageLayout.runtimeDirectory().toString())
.args(jlinkCmdLine)::executeExpectSuccess).run();
}
@Override

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2011, 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2011, 2026, 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
@ -28,7 +28,6 @@ package jdk.jpackage.internal;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
/**
* Log
@ -105,29 +104,6 @@ public class Log {
}
}
public void verbose(List<String> strings,
List<String> output, int returnCode, long pid) {
if (verbose) {
StringBuilder sb = new StringBuilder();
sb.append("Command [PID: ");
sb.append(pid);
sb.append("]:\n ");
for (String s : strings) {
sb.append(" " + s);
}
verbose(sb.toString());
if (output != null && !output.isEmpty()) {
sb = new StringBuilder("Output:");
for (String s : output) {
sb.append("\n " + s);
}
verbose(sb.toString());
}
verbose("Returned: " + returnCode + "\n");
}
}
private String addTimestamp(String msg) {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");
Date time = new Date(System.currentTimeMillis());
@ -177,9 +153,4 @@ public class Log {
public static void verbose(Throwable t) {
instance.get().verbose(t);
}
public static void verbose(List<String> strings, List<String> out,
int ret, long pid) {
instance.get().verbose(strings, out, ret, pid);
}
}

View File

@ -0,0 +1,72 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal;
import java.util.Objects;
import java.util.Optional;
import jdk.jpackage.internal.util.CompositeProxy;
interface ObjectFactory extends ExecutorFactory, RetryExecutorFactory {
static ObjectFactory.Builder build() {
return new Builder();
}
static ObjectFactory.Builder build(ObjectFactory from) {
return build().initFrom(from);
}
static final class Builder {
private Builder() {
}
ObjectFactory create() {
return CompositeProxy.build().invokeTunnel(CompositeProxyTunnel.INSTANCE).create(
ObjectFactory.class,
Optional.ofNullable(executorFactory).orElse(ExecutorFactory.DEFAULT),
Optional.ofNullable(retryExecutorFactory).orElse(RetryExecutorFactory.DEFAULT));
}
Builder initFrom(ObjectFactory of) {
Objects.requireNonNull(of);
return executorFactory(of).retryExecutorFactory(of);
}
Builder executorFactory(ExecutorFactory v) {
executorFactory = v;
return this;
}
Builder retryExecutorFactory(RetryExecutorFactory v) {
retryExecutorFactory = v;
return this;
}
private ExecutorFactory executorFactory;
private RetryExecutorFactory retryExecutorFactory;
}
static final ObjectFactory DEFAULT = build().create();
}

View File

@ -1,136 +0,0 @@
/*
* Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal;
import java.io.IOException;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
public final class RetryExecutor {
public RetryExecutor() {
setMaxAttemptsCount(5);
setAttemptTimeoutMillis(2 * 1000);
setWriteOutputToFile(false);
}
public RetryExecutor setMaxAttemptsCount(int v) {
attempts = v;
return this;
}
public RetryExecutor setAttemptTimeoutMillis(int v) {
timeoutMillis = v;
return this;
}
public RetryExecutor saveOutput(boolean v) {
saveOutput = v;
return this;
}
public List<String> getOutput() {
return output;
}
public RetryExecutor setWriteOutputToFile(boolean v) {
writeOutputToFile = v;
return this;
}
public RetryExecutor setExecutorInitializer(Consumer<Executor> v) {
executorInitializer = v;
return this;
}
public void abort() {
aborted = true;
}
public boolean isAborted() {
return aborted;
}
static RetryExecutor retryOnKnownErrorMessage(String v) {
RetryExecutor result = new RetryExecutor();
return result.setExecutorInitializer(exec -> {
exec.setOutputConsumer(output -> {
if (!output.anyMatch(v::equals)) {
result.abort();
}
});
});
}
public void execute(String cmdline[]) throws IOException {
executeLoop(() ->
Executor.of(cmdline).setWriteOutputToFile(writeOutputToFile));
}
public void execute(ProcessBuilder pb) throws IOException {
executeLoop(() ->
Executor.of(pb).setWriteOutputToFile(writeOutputToFile));
}
private void executeLoop(Supplier<Executor> execSupplier) throws IOException {
aborted = false;
for (;;) {
if (aborted) {
break;
}
try {
Executor exec = execSupplier.get().saveOutput(saveOutput);
if (executorInitializer != null) {
executorInitializer.accept(exec);
}
exec.executeExpectSuccess();
if (saveOutput) {
output = exec.getOutput();
}
break;
} catch (IOException ex) {
if (aborted || (--attempts) <= 0) {
throw ex;
}
}
try {
Thread.sleep(timeoutMillis);
} catch (InterruptedException ex) {
Log.verbose(ex);
throw new RuntimeException(ex);
}
}
}
private Consumer<Executor> executorInitializer;
private boolean aborted;
private int attempts;
private int timeoutMillis;
private boolean saveOutput;
private List<String> output;
private boolean writeOutputToFile;
}

View File

@ -0,0 +1,35 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal;
import jdk.jpackage.internal.util.RetryExecutor;
@FunctionalInterface
interface RetryExecutorFactory {
<T, E extends Exception> RetryExecutor<T, E> retryExecutor(Class<? extends E> exceptionType);
static final RetryExecutorFactory DEFAULT = RetryExecutor::new;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -24,5 +24,5 @@
*/
package jdk.jpackage.internal;
public interface SystemEnvironment {
interface SystemEnvironment {
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2026, 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
@ -121,32 +121,32 @@ final class ToolValidator {
cmdline.addAll(args);
}
boolean canUseTool[] = new boolean[1];
boolean canUseTool = false;
if (minimalVersion == null) {
// No version check.
canUseTool[0] = true;
canUseTool = true;
}
String[] version = new String[1];
String version = null;
try {
Executor.of(cmdline.toArray(String[]::new)).setQuiet(true).setOutputConsumer(lines -> {
if (versionParser != null && minimalVersion != null) {
version[0] = versionParser.apply(lines);
if (version[0] != null && minimalVersion.compareTo(version[0]) <= 0) {
canUseTool[0] = true;
}
var result = Executor.of(cmdline).setQuiet(true).saveOutput().execute();
var lines = result.content();
if (versionParser != null && minimalVersion != null) {
version = versionParser.apply(lines.stream());
if (version != null && minimalVersion.compareTo(version) <= 0) {
canUseTool = true;
}
}).execute();
}
} catch (IOException e) {
return new ConfigException(I18N.format("error.tool-error", toolPath, e.getMessage()), null, e);
}
if (canUseTool[0]) {
if (canUseTool) {
// All good. Tool can be used.
return null;
} else if (toolOldVersionErrorHandler != null) {
return toolOldVersionErrorHandler.apply(toolPath, version[0]);
return toolOldVersionErrorHandler.apply(toolPath, version);
} else {
return new ConfigException(
I18N.format("error.tool-old-version", toolPath, minimalVersion),

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -45,6 +45,7 @@ import java.util.function.Supplier;
import java.util.spi.ToolProvider;
import jdk.internal.opt.CommandLine;
import jdk.internal.util.OperatingSystem;
import jdk.jpackage.internal.Globals;
import jdk.jpackage.internal.Log;
import jdk.jpackage.internal.model.ConfigException;
import jdk.jpackage.internal.model.JPackageException;
@ -56,7 +57,15 @@ import jdk.jpackage.internal.util.function.ExceptionBox;
*/
public final class Main {
public static final class Provider implements ToolProvider {
public record Provider(Supplier<CliBundlingEnvironment> bundlingEnvSupplier) implements ToolProvider {
public Provider {
Objects.requireNonNull(bundlingEnvSupplier);
}
public Provider() {
this(Main::loadBundlingEnvironment);
}
@Override
public String name() {
@ -65,7 +74,7 @@ public final class Main {
@Override
public int run(PrintWriter out, PrintWriter err, String... args) {
return Main.run(out, err, args);
return Main.run(bundlingEnvSupplier, out, err, args);
}
@Override
@ -94,7 +103,23 @@ public final class Main {
System.exit(run(out, err, args));
}
public static int run(PrintWriter out, PrintWriter err, String... args) {
static int run(PrintWriter out, PrintWriter err, String... args) {
return run(Main::loadBundlingEnvironment, out, err, args);
}
static int run(Supplier<CliBundlingEnvironment> bundlingEnvSupplier, PrintWriter out, PrintWriter err, String... args) {
return Globals.main(() -> {
return runWithGlobals(bundlingEnvSupplier, out, err, args);
});
}
private static int runWithGlobals(
Supplier<CliBundlingEnvironment> bundlingEnvSupplier,
PrintWriter out,
PrintWriter err,
String... args) {
Objects.requireNonNull(bundlingEnvSupplier);
Objects.requireNonNull(args);
for (String arg : args) {
Objects.requireNonNull(arg);
@ -128,8 +153,7 @@ public final class Main {
return preprocessStatus;
}
final var bundlingEnv = ServiceLoader.load(CliBundlingEnvironment.class,
CliBundlingEnvironment.class.getClassLoader()).findFirst().orElseThrow();
final var bundlingEnv = bundlingEnvSupplier.get();
final var parseResult = Utils.buildParser(OperatingSystem.current(), bundlingEnv).create().apply(mappedArgs.get());
@ -285,4 +309,10 @@ public final class Main {
private static String getVersion() {
return System.getProperty("java.version");
}
private static CliBundlingEnvironment loadBundlingEnvironment() {
return ServiceLoader.load(
CliBundlingEnvironment.class,
CliBundlingEnvironment.class.getClassLoader()).findFirst().orElseThrow();
}
}

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved.
# Copyright (c) 2017, 2026, 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
@ -96,7 +96,6 @@ error.tool-not-found.advice=Please install "{0}"
error.tool-old-version=Can not find "{0}" {1} or newer
error.tool-old-version.advice=Please install "{0}" {1} or newer
error.jlink.failed=jlink failed with: {0}
error.blocked.option=jlink option [{0}] is not permitted in --jlink-options
error.no.name=Name not specified with --name and cannot infer one from app-image
error.no.name.advice=Specify name with --name

View File

@ -0,0 +1,52 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal.util;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Formats command line arguments.
*/
public final class CommandLineFormat {
public String format(List<String> cmdline) {
return cmdline.stream().map(enquoter::applyTo).collect(Collectors.joining(" "));
}
public static CommandLineFormat platform() {
var format = new CommandLineFormat();
format.enquoter = Enquoter.identity().setEnquotePredicate(Enquoter.QUOTE_IF_WHITESPACES).setQuoteChar('\'');
return format;
}
private CommandLineFormat() {
}
private Enquoter enquoter;
public static final Function<List<String>, String> DEFAULT = platform()::format;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2022, 2026, 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,7 +22,7 @@
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal;
package jdk.jpackage.internal.util;
import java.util.Optional;
import java.util.function.BiConsumer;
@ -32,39 +32,43 @@ import java.util.regex.Pattern;
/**
* Add quotes to the given string in a configurable way.
*/
final class Enquoter {
public final class Enquoter {
private Enquoter() {
setQuoteChar('"');
}
static Enquoter forPropertyValues() {
public static Enquoter identity() {
return new Enquoter();
}
public static Enquoter forPropertyValues() {
return new Enquoter()
.setEnquotePredicate(QUOTE_IF_WHITESPACES)
.setEscaper(PREPEND_BACKSLASH);
}
static Enquoter forShellLiterals() {
public static Enquoter forShellLiterals() {
return forShellLiterals('\'');
}
static Enquoter forShellLiterals(char quoteChar) {
public static Enquoter forShellLiterals(char quoteChar) {
return new Enquoter()
.setQuoteChar(quoteChar)
.setEnquotePredicate(x -> true)
.setEscaper(PREPEND_BACKSLASH);
}
String applyTo(String v) {
public String applyTo(String v) {
if (!needQuotes.test(v)) {
return v;
} else {
var buf = new StringBuilder();
buf.appendCodePoint(beginQuoteChr);
Optional.of(escaper).ifPresentOrElse(op -> {
Optional.ofNullable(escaper).ifPresentOrElse(op -> {
v.codePoints().forEachOrdered(chr -> {
if (chr == beginQuoteChr || chr == endQuoteChr) {
escaper.accept(chr, buf);
op.accept(chr, buf);
} else {
buf.appendCodePoint(chr);
}
@ -77,28 +81,23 @@ final class Enquoter {
}
}
Enquoter setQuoteChar(char chr) {
public Enquoter setQuoteChar(char chr) {
beginQuoteChr = chr;
endQuoteChr = chr;
return this;
}
Enquoter setEscaper(BiConsumer<Integer, StringBuilder> v) {
public Enquoter setEscaper(BiConsumer<Integer, StringBuilder> v) {
escaper = v;
return this;
}
Enquoter setEnquotePredicate(Predicate<String> v) {
public Enquoter setEnquotePredicate(Predicate<String> v) {
needQuotes = v;
return this;
}
private int beginQuoteChr;
private int endQuoteChr;
private BiConsumer<Integer, StringBuilder> escaper;
private Predicate<String> needQuotes = str -> false;
private static final Predicate<String> QUOTE_IF_WHITESPACES = new Predicate<String>() {
public static final Predicate<String> QUOTE_IF_WHITESPACES = new Predicate<String>() {
@Override
public boolean test(String t) {
return pattern.matcher(t).find();
@ -106,8 +105,13 @@ final class Enquoter {
private final Pattern pattern = Pattern.compile("\\s");
};
private static final BiConsumer<Integer, StringBuilder> PREPEND_BACKSLASH = (chr, buf) -> {
public static final BiConsumer<Integer, StringBuilder> PREPEND_BACKSLASH = (chr, buf) -> {
buf.append('\\');
buf.appendCodePoint(chr);
};
private int beginQuoteChr;
private int endQuoteChr;
private BiConsumer<Integer, StringBuilder> escaper;
private Predicate<String> needQuotes = str -> false;
}

View File

@ -0,0 +1,194 @@
/*
* Copyright (c) 2020, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal.util;
import static jdk.jpackage.internal.util.function.ThrowingConsumer.toConsumer;
import java.time.Duration;
import java.util.Iterator;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import jdk.jpackage.internal.util.function.ExceptionBox;
import jdk.jpackage.internal.util.function.ThrowingFunction;
import jdk.jpackage.internal.util.function.ThrowingSupplier;
public class RetryExecutor<T, E extends Exception> {
public RetryExecutor(Class<? extends E> exceptionType) {
this.exceptionType = Objects.requireNonNull(exceptionType);
setMaxAttemptsCount(5);
setAttemptTimeout(2, TimeUnit.SECONDS);
}
final public Class<? extends E> exceptionType() {
return exceptionType;
}
public RetryExecutor<T, E> setExecutable(ThrowingFunction<Context<RetryExecutor<T, E>>, T, E> v) {
executable = v;
return this;
}
final public RetryExecutor<T, E> setExecutable(ThrowingSupplier<T, E> v) {
if (v != null) {
setExecutable(_ -> {
return v.get();
});
} else {
executable = null;
}
return this;
}
public RetryExecutor<T, E> setMaxAttemptsCount(int v) {
attempts = v;
return this;
}
final public RetryExecutor<T, E> setAttemptTimeout(long v, TimeUnit unit) {
return setAttemptTimeout(Duration.of(v, unit.toChronoUnit()));
}
public RetryExecutor<T, E> setAttemptTimeout(Duration v) {
timeout = v;
return this;
}
public RetryExecutor<T, E> setExceptionMapper(Function<E, RuntimeException> v) {
toUnchecked = v;
return this;
}
public RetryExecutor<T, E> setSleepFunction(Consumer<Duration> v) {
sleepFunction = v;
return this;
}
final public RetryExecutor<T, E> mutate(Consumer<RetryExecutor<T, E>> mutator) {
mutator.accept(this);
return this;
}
public T execute() throws E {
var curExecutable = executable();
T result = null;
var attemptIter = new DefaultContext();
while (attemptIter.hasNext()) {
attemptIter.next();
try {
result = curExecutable.apply(attemptIter);
break;
} catch (Exception ex) {
if (!exceptionType.isInstance(ex)) {
throw ExceptionBox.toUnchecked(ex);
} else if (attemptIter.isLastAttempt()) {
// No more attempts left. This is fatal.
throw exceptionType.cast(ex);
} else {
curExecutable = executable();
}
}
sleep();
}
return result;
}
final public T executeUnchecked() {
try {
return execute();
} catch (Error | RuntimeException t) {
throw t;
} catch (Exception ex) {
if (exceptionType.isInstance(ex)) {
throw Optional.ofNullable(toUnchecked).orElse(ExceptionBox::toUnchecked).apply(exceptionType.cast(ex));
} else {
// Unreachable unless it is a direct subclass of Throwable,
// which is not Error or Exception which should not happen.
throw ExceptionBox.reachedUnreachable();
}
}
}
public interface Context<T> {
boolean isLastAttempt();
int attempt();
T executor();
}
private final class DefaultContext implements Context<RetryExecutor<T, E>>, Iterator<Void> {
@Override
public boolean isLastAttempt() {
return !hasNext();
}
@Override
public int attempt() {
return attempt;
}
@Override
public boolean hasNext() {
return (attempts - attempt) > 1;
}
@Override
public Void next() {
attempt++;
return null;
}
@Override
public RetryExecutor<T, E> executor() {
return RetryExecutor.this;
}
private int attempt = -1;
}
private ThrowingFunction<Context<RetryExecutor<T, E>>, T, E> executable() {
return Optional.ofNullable(executable).orElseThrow(() -> {
return new IllegalStateException("No executable");
});
}
private void sleep() {
Optional.ofNullable(timeout).ifPresent(Optional.ofNullable(sleepFunction).orElseGet(() -> {
return toConsumer(Thread::sleep);
}));
}
private final Class<? extends E> exceptionType;
private ThrowingFunction<Context<RetryExecutor<T, E>>, T, E> executable;
private int attempts;
private Duration timeout;
private Function<E, RuntimeException> toUnchecked;
private Consumer<Duration> sleepFunction;
}

View File

@ -0,0 +1,89 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal.util;
import java.io.Closeable;
import java.io.Flushable;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Objects;
import jdk.jpackage.internal.util.function.ThrowingConsumer;
public final class TeeOutputStream extends OutputStream {
public TeeOutputStream(Iterable<OutputStream> items) {
items.forEach(Objects::requireNonNull);
this.items = items;
}
@Override
public void write(int b) throws IOException {
for (final var item : items) {
item.write(b);
}
}
@Override
public void write(byte[] b) throws IOException {
for (final var item : items) {
item.write(b);
}
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
for (final var item : items) {
item.write(b, off, len);
}
}
@Override
public void flush() throws IOException {
forEach(Flushable::flush);
}
@Override
public void close() throws IOException {
forEach(Closeable::close);
}
private void forEach(ThrowingConsumer<OutputStream, IOException> c) throws IOException {
IOException firstEx = null;
for (final var item : items) {
try {
c.accept(item);
} catch (IOException e) {
if (firstEx == null) {
firstEx = e;
}
}
}
if (firstEx != null) {
throw firstEx;
}
}
private final Iterable<OutputStream> items;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2022, 2026, 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
@ -24,8 +24,6 @@
*/
package jdk.jpackage.internal;
import jdk.jpackage.internal.model.Launcher;
import jdk.jpackage.internal.model.Application;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collections;
@ -36,6 +34,9 @@ import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jdk.jpackage.internal.model.Application;
import jdk.jpackage.internal.model.Launcher;
import jdk.jpackage.internal.util.Enquoter;
/**
* Helper to install launchers as services for Unix installers.

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -31,16 +31,17 @@ import static jdk.jpackage.internal.cli.StandardBundlingOperation.CREATE_WIN_EXE
import static jdk.jpackage.internal.cli.StandardBundlingOperation.CREATE_WIN_MSI;
import jdk.jpackage.internal.cli.Options;
import jdk.jpackage.internal.util.Result;
public class WinBundlingEnvironment extends DefaultBundlingEnvironment {
public WinBundlingEnvironment() {
super(build()
.defaultOperation(CREATE_WIN_EXE)
.bundler(CREATE_WIN_APP_IMAGE, WinBundlingEnvironment::createAppImage)
.bundler(CREATE_WIN_EXE, LazyLoad::sysEnv, WinBundlingEnvironment::createExePackage)
.bundler(CREATE_WIN_MSI, LazyLoad::sysEnv, WinBundlingEnvironment::createMsiPackage));
super(build().mutate(builder -> {
var sysEnv = runOnce(WinSystemEnvironment::create);
builder
.bundler(CREATE_WIN_EXE, sysEnv, WinBundlingEnvironment::createExePackage)
.bundler(CREATE_WIN_MSI, sysEnv, WinBundlingEnvironment::createMsiPackage);
}).defaultOperation(CREATE_WIN_EXE).bundler(CREATE_WIN_APP_IMAGE, WinBundlingEnvironment::createAppImage));
}
private static void createMsiPackage(Options options, WinSystemEnvironment sysEnv) {
@ -98,12 +99,4 @@ public class WinBundlingEnvironment extends DefaultBundlingEnvironment {
}
}
private static final class LazyLoad {
static Result<WinSystemEnvironment> sysEnv() {
return SYS_ENV;
}
private static final Result<WinSystemEnvironment> SYS_ENV = WinSystemEnvironment.create();
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2026, 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
@ -233,10 +233,10 @@ public enum WixTool {
// Detect FIPS mode
var fips = false;
try {
final var exec = Executor.of(toolPath.toString(), "-?").setQuiet(true).saveOutput(true);
final var exitCode = exec.execute();
final var result = Executor.of(toolPath.toString(), "-?").setQuiet(true).saveOutput(true).execute();
final var exitCode = result.getExitCode();
if (exitCode != 0 /* 308 */) {
final var output = exec.getOutput();
final var output = result.getOutput();
if (!output.isEmpty() && output.get(0).contains("error CNDL0308")) {
fips = true;
}

View File

@ -1,401 +0,0 @@
/*
* 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 static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toSet;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.UncheckedIOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import java.util.spi.ToolProvider;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
public class ExecutorTest extends JUnitAdapter {
private record Command(List<String> stdout, List<String> stderr) {
Command {
stdout.forEach(Objects::requireNonNull);
stderr.forEach(Objects::requireNonNull);
}
List<String> asExecutable() {
final List<String> commandline = new ArrayList<>();
if (TKit.isWindows()) {
commandline.addAll(List.of("cmd", "/C"));
} else {
commandline.addAll(List.of("sh", "-c"));
}
commandline.add(Stream.concat(createEchoCommands(stdout),
createEchoCommands(stderr).map(v -> v + ">&2")).collect(joining(" && ")));
return commandline;
}
private static Stream<String> createEchoCommands(List<String> lines) {
return lines.stream().map(line -> {
if (TKit.isWindows()) {
return "(echo " + line + ")";
} else {
return "echo " + line;
}
});
}
ToolProvider asToolProvider() {
return new ToolProvider() {
@Override
public int run(PrintWriter out, PrintWriter err, String... args) {
stdout.forEach(out::println);
stderr.forEach(err::println);
return 0;
}
@Override
public String name() {
return "test";
}
};
}
}
private enum OutputData {
EMPTY(List.of()),
ONE_LINE(List.of("Jupiter")),
MANY(List.of("Uranus", "Saturn", "Earth"));
OutputData(List<String> data) {
data.forEach(Objects::requireNonNull);
this.data = data;
}
final List<String> data;
}
private record CommandSpec(OutputData stdout, OutputData stderr) {
CommandSpec {
Objects.requireNonNull(stdout);
Objects.requireNonNull(stderr);
}
Command command() {
return new Command(stdout.data.stream().map(line -> {
return "stdout." + line;
}).toList(), stderr.data.stream().map(line -> {
return "stderr." + line;
}).toList());
}
}
public enum OutputControl {
DUMP(Executor::dumpOutput),
SAVE_ALL(Executor::saveOutput),
SAVE_FIRST_LINE(Executor::saveFirstLineOfOutput),
DISCARD_STDOUT(Executor::discardStdout),
DISCARD_STDERR(Executor::discardStderr),
;
OutputControl(Consumer<Executor> configureExector) {
this.configureExector = Objects.requireNonNull(configureExector);
}
Executor applyTo(Executor exec) {
configureExector.accept(exec);
return exec;
}
static List<Set<OutputControl>> variants() {
final List<Set<OutputControl>> variants = new ArrayList<>();
for (final var withDump : BOOLEAN_VALUES) {
variants.addAll(Stream.of(
Set.<OutputControl>of(),
Set.of(SAVE_ALL),
Set.of(SAVE_FIRST_LINE),
Set.of(DISCARD_STDOUT),
Set.of(DISCARD_STDERR),
Set.of(SAVE_ALL, DISCARD_STDOUT),
Set.of(SAVE_FIRST_LINE, DISCARD_STDOUT),
Set.of(SAVE_ALL, DISCARD_STDERR),
Set.of(SAVE_FIRST_LINE, DISCARD_STDERR),
Set.of(SAVE_ALL, DISCARD_STDOUT, DISCARD_STDERR),
Set.of(SAVE_FIRST_LINE, DISCARD_STDOUT, DISCARD_STDERR)
).map(v -> {
if (withDump) {
return Stream.concat(Stream.of(DUMP), v.stream()).collect(toSet());
} else {
return v;
}
}).toList());
}
return variants.stream().map(options -> {
return options.stream().filter(o -> {
return o.configureExector != NOP;
}).collect(toSet());
}).distinct().toList();
}
private final Consumer<Executor> configureExector;
static final Set<OutputControl> SAVE = Set.of(SAVE_ALL, SAVE_FIRST_LINE);
}
public record OutputTestSpec(boolean toolProvider, Set<OutputControl> outputControl, CommandSpec commandSpec) {
public OutputTestSpec {
outputControl.forEach(Objects::requireNonNull);
if (outputControl.containsAll(OutputControl.SAVE)) {
throw new IllegalArgumentException();
}
Objects.requireNonNull(commandSpec);
}
@Override
public String toString() {
final List<String> tokens = new ArrayList<>();
if (toolProvider) {
tokens.add("tool-provider");
}
tokens.add("output=" + format(outputControl));
tokens.add("command=" + commandSpec);
return String.join(",", tokens.toArray(String[]::new));
}
void test() {
final var command = commandSpec.command();
final var commandWithDiscardedStreams = discardStreams(command);
final Executor.Result[] result = new Executor.Result[1];
final var outputCapture = OutputCapture.captureOutput(() -> {
result[0] = createExecutor(command).executeWithoutExitCodeCheck();
});
assertEquals(0, result[0].getExitCode());
// If we dump the subprocesses's output, and the command produced both STDOUT and STDERR,
// then the captured STDOUT may contain interleaved command's STDOUT and STDERR,
// not in sequential order (STDOUT followed by STDERR).
// In this case don't check the contents of the captured command's STDOUT.
if (toolProvider || outputCapture.outLines().isEmpty() || (command.stdout().isEmpty() || command.stderr().isEmpty())) {
assertEquals(expectedCapturedSystemOut(commandWithDiscardedStreams), outputCapture.outLines());
}
assertEquals(expectedCapturedSystemErr(commandWithDiscardedStreams), outputCapture.errLines());
assertEquals(expectedResultStdout(commandWithDiscardedStreams), result[0].stdout().getOutput());
assertEquals(expectedResultStderr(commandWithDiscardedStreams), result[0].stderr().getOutput());
if (!saveOutput()) {
assertNull(result[0].getOutput());
} else {
assertNotNull(result[0].getOutput());
final var allExpectedOutput = expectedCommandOutput(command);
assertEquals(allExpectedOutput.isEmpty(), result[0].getOutput().isEmpty());
if (!allExpectedOutput.isEmpty()) {
if (outputControl.contains(OutputControl.SAVE_ALL)) {
assertEquals(allExpectedOutput, result[0].getOutput());
} else if (outputControl.contains(OutputControl.SAVE_FIRST_LINE)) {
assertEquals(1, result[0].getOutput().size());
assertEquals(allExpectedOutput.getFirst(), result[0].getFirstLineOfOutput());
} else {
throw new UnsupportedOperationException();
}
}
}
}
private boolean dumpOutput() {
return outputControl.contains(OutputControl.DUMP);
}
private boolean saveOutput() {
return !Collections.disjoint(outputControl, OutputControl.SAVE);
}
private boolean discardStdout() {
return outputControl.contains(OutputControl.DISCARD_STDOUT);
}
private boolean discardStderr() {
return outputControl.contains(OutputControl.DISCARD_STDERR);
}
private static String format(Set<OutputControl> outputControl) {
return outputControl.stream().map(OutputControl::name).sorted().collect(joining("+"));
}
private List<String> expectedCapturedSystemOut(Command command) {
if (!dumpOutput() || (!toolProvider && !saveOutput())) {
return List.of();
} else if(saveOutput()) {
return Stream.concat(command.stdout().stream(), command.stderr().stream()).toList();
} else {
return command.stdout();
}
}
private List<String> expectedCapturedSystemErr(Command command) {
if (!dumpOutput() || (!toolProvider && !saveOutput())) {
return List.of();
} else if(saveOutput()) {
return List.of();
} else {
return command.stderr();
}
}
private List<String> expectedResultStdout(Command command) {
return expectedResultStream(command.stdout());
}
private List<String> expectedResultStderr(Command command) {
if (outputControl.contains(OutputControl.SAVE_FIRST_LINE) && !command.stdout().isEmpty()) {
return List.of();
}
return expectedResultStream(command.stderr());
}
private List<String> expectedResultStream(List<String> commandOutput) {
Objects.requireNonNull(commandOutput);
if (outputControl.contains(OutputControl.SAVE_ALL)) {
return commandOutput;
} else if (outputControl.contains(OutputControl.SAVE_FIRST_LINE)) {
return commandOutput.stream().findFirst().map(List::of).orElseGet(List::of);
} else {
return null;
}
}
private Command discardStreams(Command command) {
return new Command(discardStdout() ? List.of() : command.stdout(), discardStderr() ? List.of() : command.stderr());
}
private record OutputCapture(byte[] out, byte[] err, Charset outCharset, Charset errCharset) {
OutputCapture {
Objects.requireNonNull(out);
Objects.requireNonNull(err);
Objects.requireNonNull(outCharset);
Objects.requireNonNull(errCharset);
}
List<String> outLines() {
return toLines(out, outCharset);
}
List<String> errLines() {
return toLines(err, errCharset);
}
private static List<String> toLines(byte[] buf, Charset charset) {
try (var reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf), charset))) {
return reader.lines().filter(line -> {
return !line.contains("TRACE");
}).toList();
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
static OutputCapture captureOutput(Runnable runnable) {
final var captureOut = new ByteArrayOutputStream();
final var captureErr = new ByteArrayOutputStream();
final var out = System.out;
final var err = System.err;
try {
final var outCharset = System.out.charset();
final var errCharset = System.err.charset();
System.setOut(new PrintStream(captureOut, true, outCharset));
System.setErr(new PrintStream(captureErr, true, errCharset));
runnable.run();
return new OutputCapture(captureOut.toByteArray(), captureErr.toByteArray(), outCharset, errCharset);
} finally {
try {
System.setOut(out);
} finally {
System.setErr(err);
}
}
}
}
private List<String> expectedCommandOutput(Command command) {
command = discardStreams(command);
return Stream.of(command.stdout(), command.stderr()).flatMap(List::stream).toList();
}
private Executor createExecutor(Command command) {
final Executor exec;
if (toolProvider) {
exec = Executor.of(command.asToolProvider());
} else {
exec = Executor.of(command.asExecutable());
}
outputControl.forEach(control -> control.applyTo(exec));
return exec;
}
}
@ParameterizedTest
@MethodSource
public void testSavedOutput(OutputTestSpec spec) {
spec.test();
}
public static List<OutputTestSpec> testSavedOutput() {
List<OutputTestSpec> testCases = new ArrayList<>();
for (final var toolProvider : BOOLEAN_VALUES) {
for (final var outputControl : OutputControl.variants()) {
for (final var stdoutContent : List.of(OutputData.values())) {
for (final var stderrContent : List.of(OutputData.values())) {
final var commandSpec = new CommandSpec(stdoutContent, stderrContent);
testCases.add(new OutputTestSpec(toolProvider, outputControl, commandSpec));
}
}
}
}
return testCases;
}
private static final List<Boolean> BOOLEAN_VALUES = List.of(Boolean.TRUE, Boolean.FALSE);
private static final Consumer<Executor> NOP = exec -> {};
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -213,7 +213,7 @@ public class PackageTestTest extends JUnitAdapter {
@Override
public void accept(JPackageCommand cmd, Executor.Result result) {
tick();
jpackageExitCode = result.exitCode();
jpackageExitCode = result.getExitCode();
}
@Override
@ -371,8 +371,7 @@ public class PackageTestTest extends JUnitAdapter {
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
return new Executor.Result(actualJPackageExitCode,
this::getPrintableCommandLine).assertExitCodeIs(expectedExitCode);
return new Executor.Result(actualJPackageExitCode).assertExitCodeIs(expectedExitCode);
}
};
}).setExpectedExitCode(expectedJPackageExitCode)

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2026, 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,18 +22,9 @@
*/
package jdk.jpackage.test;
import static java.util.stream.Collectors.joining;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.StringReader;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
@ -43,15 +34,17 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import java.util.spi.ToolProvider;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import jdk.jpackage.internal.util.function.ThrowingSupplier;
import jdk.jpackage.internal.util.CommandLineFormat;
import jdk.jpackage.internal.util.CommandOutputControl;
import jdk.jpackage.internal.util.CommandOutputControl.UnexpectedExitCodeException;
import jdk.jpackage.internal.util.RetryExecutor;
import jdk.jpackage.internal.util.function.ExceptionBox;
import jdk.jpackage.internal.util.function.ThrowingSupplier;
public final class Executor extends CommandArguments<Executor> {
@ -69,8 +62,6 @@ public final class Executor extends CommandArguments<Executor> {
}
public Executor() {
outputStreamsControl = new OutputStreamsControl();
winEnglishOutput = false;
}
public Executor setExecutable(String v) {
@ -136,62 +127,31 @@ public final class Executor extends CommandArguments<Executor> {
return this;
}
/**
* Configures this instance to save all stdout and stderr streams from the to be
* executed command.
* <p>
* This function is mutually exclusive with {@link #saveFirstLineOfOutput()}.
*
* @return this
*/
public Executor saveOutput() {
return saveOutput(true);
}
/**
* Configures if all stdout and stderr streams from the to be executed command
* should be saved.
* <p>
* If <code>v</code> is <code>true</code>, the function call is equivalent to
* {@link #saveOutput()} call. If <code>v</code> is <code>false</code>, command
* output will not be saved.
*
* @parameter v if both stdout and stderr streams should be saved
*
* @return this
*/
public Executor saveOutput(boolean v) {
return setOutputControl(v, OutputControlOption.SAVE_ALL);
commandOutputControl.saveOutput(v);
return this;
}
/**
* Configures this instance to save the first line of a stream merged from
* stdout and stderr streams from the to be executed command.
* <p>
* This function is mutually exclusive with {@link #saveOutput()}.
*
* @return this
*/
public Executor saveFirstLineOfOutput() {
return setOutputControl(true, OutputControlOption.SAVE_FIRST_LINE);
commandOutputControl.saveFirstLineOfOutput();
return this;
}
/**
* Configures this instance to dump both stdout and stderr streams from the to
* be executed command into {@link System.out}.
*
* @return this
*/
public Executor dumpOutput() {
return dumpOutput(true);
}
public Executor dumpOutput(boolean v) {
return setOutputControl(v, OutputControlOption.DUMP);
commandOutputControl.dumpOutput(v);
return this;
}
public Executor discardStdout(boolean v) {
outputStreamsControl.stdout().discard(v);
commandOutputControl.discardStdout(v);
return this;
}
@ -200,7 +160,7 @@ public final class Executor extends CommandArguments<Executor> {
}
public Executor discardStderr(boolean v) {
outputStreamsControl.stderr().discard(v);
commandOutputControl.discardStderr(v);
return this;
}
@ -208,45 +168,126 @@ public final class Executor extends CommandArguments<Executor> {
return discardStderr(true);
}
public interface Output {
public List<String> getOutput();
public default String getFirstLineOfOutput() {
return findFirstLineOfOutput().orElseThrow();
}
public default Optional<String> findFirstLineOfOutput() {
return getOutput().stream().findFirst();
}
public Executor binaryOutput(boolean v) {
commandOutputControl.binaryOutput(v);
return this;
}
public record Result(int exitCode, CommandOutput output, Supplier<String> cmdline) implements Output {
public Executor binaryOutput() {
return binaryOutput(true);
}
public Executor charset(Charset v) {
commandOutputControl.charset(v);
return this;
}
public Charset charset() {
return commandOutputControl.charset();
}
Executor storeOutputInFiles(boolean v) {
commandOutputControl.storeOutputInFiles(v);
return this;
}
Executor storeOutputInFiles() {
return storeOutputInFiles(true);
}
public record Result(CommandOutputControl.Result base) {
public Result {
Objects.requireNonNull(output);
Objects.requireNonNull(cmdline);
Objects.requireNonNull(base);
}
public Result(int exitCode, Supplier<String> cmdline) {
this(exitCode, CommandOutput.EMPTY, cmdline);
public Result(int exitCode) {
this(new CommandOutputControl.Result(exitCode));
}
@Override
public List<String> getOutput() {
return output.lines().orElse(null);
return base.content();
}
public Output stdout() {
return createView(output.stdoutLines());
public String getFirstLineOfOutput() {
return getOutput().getFirst();
}
public Output stderr() {
return createView(output.stderrLines());
public List<String> stdout() {
return base.stdout();
}
public Result assertExitCodeIs(int expectedExitCode) {
TKit.assertEquals(expectedExitCode, exitCode, String.format(
"Check command %s exited with %d code",
cmdline.get(), expectedExitCode));
public List<String> stderr() {
return base.stderr();
}
public Optional<List<String>> findContent() {
return base.findContent();
}
public Optional<List<String>> findStdout() {
return base.findStdout();
}
public Optional<List<String>> findStderr() {
return base.findStderr();
}
public byte[] byteContent() {
return base.byteContent();
}
public byte[] byteStdout() {
return base.byteStdout();
}
public byte[] byteStderr() {
return base.byteStderr();
}
public Optional<byte[]> findByteContent() {
return base.findByteContent();
}
public Optional<byte[]> findByteStdout() {
return base.findByteStdout();
}
public Optional<byte[]> findByteStderr() {
return base.findByteStderr();
}
public Result toCharacterResult(Charset charset, boolean keepByteContent) {
try {
return new Result(base.toCharacterResult(charset, keepByteContent));
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
public Result assertExitCodeIs(int main, int... other) {
if (other.length != 0) {
return assertExitCodeIs(IntStream.concat(IntStream.of(main), IntStream.of(other)).boxed().toList());
} else {
return assertExitCodeIs(List.of(main));
}
}
private Result assertExitCodeIs(List<Integer> expectedExitCodes) {
Objects.requireNonNull(expectedExitCodes);
switch (expectedExitCodes.size()) {
case 0 -> {
throw new IllegalArgumentException();
} case 1 -> {
long expectedExitCode = expectedExitCodes.getFirst();
TKit.assertEquals(expectedExitCode, getExitCode(), String.format(
"Check command %s exited with %d code",
base.execAttrs(), expectedExitCode));
} default -> {
TKit.assertTrue(expectedExitCodes.contains(getExitCode()), String.format(
"Check command %s exited with one of %s codes",
base.execAttrs(), expectedExitCodes.stream().sorted().toList()));
}
}
return this;
}
@ -255,16 +296,11 @@ public final class Executor extends CommandArguments<Executor> {
}
public int getExitCode() {
return exitCode;
return base.getExitCode();
}
private static Output createView(Optional<List<String>> lines) {
return new Output() {
@Override
public List<String> getOutput() {
return lines.orElse(null);
}
};
public String getPrintableCommandLine() {
return base.execAttrs().toString();
}
}
@ -292,8 +328,8 @@ public final class Executor extends CommandArguments<Executor> {
}).get();
}
public Result execute(int expectedCode) {
return executeWithoutExitCodeCheck().assertExitCodeIs(expectedCode);
Result execute(int mainExitCode, int... otherExitCodes) {
return executeWithoutExitCodeCheck().assertExitCodeIs(mainExitCode, otherExitCodes);
}
public Result execute() {
@ -301,28 +337,36 @@ public final class Executor extends CommandArguments<Executor> {
}
public String executeAndGetFirstLineOfOutput() {
return saveFirstLineOfOutput().execute().getFirstLineOfOutput();
return saveFirstLineOfOutput().execute().getOutput().getFirst();
}
public List<String> executeAndGetOutput() {
return saveOutput().execute().getOutput();
}
private static class BadResultException extends RuntimeException {
BadResultException(Result v) {
value = v;
private static class FailedAttemptException extends Exception {
FailedAttemptException(Exception cause) {
super(Objects.requireNonNull(cause));
}
Result getValue() {
return value;
}
private final transient Result value;
private static final long serialVersionUID = 1L;
}
public RetryExecutor<Result, UnexpectedExitCodeException> retryUntilExitCodeIs(
int mainExpectedExitCode, int... otherExpectedExitCodes) {
return new RetryExecutor<Result, UnexpectedExitCodeException>(UnexpectedExitCodeException.class).setExecutable(() -> {
var result = executeWithoutExitCodeCheck();
result.base().expectExitCode(mainExpectedExitCode, otherExpectedExitCodes);
return result;
}).setExceptionMapper((UnexpectedExitCodeException ex) -> {
createResult(ex.getResult()).assertExitCodeIs(mainExpectedExitCode, otherExpectedExitCodes);
// Unreachable, because the above `Result.assertExitCodeIs(...)` must throw.
throw ExceptionBox.reachedUnreachable();
});
}
/**
* Executes the configured command {@code max} at most times and waits for
* Executes the configured command at most {@code max} times and waits for
* {@code wait} seconds between each execution until the command exits with
* {@code expectedCode} exit code.
*
@ -332,17 +376,10 @@ public final class Executor extends CommandArguments<Executor> {
* command
*/
public Result executeAndRepeatUntilExitCode(int expectedExitCode, int max, int wait) {
try {
return tryRunMultipleTimes(() -> {
Result result = executeWithoutExitCodeCheck();
if (result.getExitCode() != expectedExitCode) {
throw new BadResultException(result);
}
return result;
}, max, wait).assertExitCodeIs(expectedExitCode);
} catch (BadResultException ex) {
return ex.getValue().assertExitCodeIs(expectedExitCode);
}
return retryUntilExitCodeIs(expectedExitCode)
.setAttemptTimeout(wait, TimeUnit.SECONDS)
.setMaxAttemptsCount(max)
.executeUnchecked();
}
/**
@ -359,26 +396,16 @@ public final class Executor extends CommandArguments<Executor> {
* @param wait number of seconds to wait between executions of the
*/
public static <T> T tryRunMultipleTimes(Supplier<T> task, int max, int wait) {
RuntimeException lastException = null;
int count = 0;
do {
return new RetryExecutor<T, FailedAttemptException>(FailedAttemptException.class).setExecutable(() -> {
try {
return task.get();
} catch (RuntimeException ex) {
lastException = ex;
throw new FailedAttemptException(ex);
}
}).setExceptionMapper((FailedAttemptException ex) -> {
return (RuntimeException)ex.getCause();
}).setAttemptTimeout(wait, TimeUnit.SECONDS).setMaxAttemptsCount(max).executeUnchecked();
try {
Thread.sleep(wait * 1000);
} catch (InterruptedException ex) {
throw new RuntimeException(ex);
}
count++;
} while (count < max);
throw lastException;
}
public static void tryRunMultipleTimes(Runnable task, int max, int wait) {
@ -392,12 +419,6 @@ public final class Executor extends CommandArguments<Executor> {
return saveOutput().executeWithoutExitCodeCheck().getOutput();
}
private Executor setOutputControl(boolean set, OutputControlOption v) {
outputStreamsControl.stdout().set(set, v);
outputStreamsControl.stderr().set(set, v);
return this;
}
private Path executablePath() {
if (directory == null
|| executable.isAbsolute()
@ -431,12 +452,8 @@ public final class Executor extends CommandArguments<Executor> {
builder.environment().put("TMP", winTmpDir);
}
outputStreamsControl.applyTo(builder);
StringBuilder sb = new StringBuilder(getPrintableCommandLine());
outputStreamsControl.describe().ifPresent(desc -> {
sb.append("; ").append(desc);
});
sb.append("; ").append(commandOutputControl.description());
if (directory != null) {
builder.directory(directory.toFile());
@ -466,141 +483,31 @@ public final class Executor extends CommandArguments<Executor> {
});
}
trace("Execute " + sb.toString() + "...");
Process process = builder.start();
var stdoutGobbler = CompletableFuture.<Optional<List<String>>>supplyAsync(() -> {
try {
return processProcessStream(outputStreamsControl.stdout(), process.getInputStream());
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
});
var stderrGobbler = CompletableFuture.<Optional<List<String>>>supplyAsync(() -> {
try {
return processProcessStream(outputStreamsControl.stderr(), process.getErrorStream());
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
});
final CommandOutput output;
try {
output = combine(stdoutGobbler.join(), stderrGobbler.join());
} catch (CompletionException ex) {
var cause = ex.getCause();
switch (cause) {
case UncheckedIOException uioex -> {
throw uioex.getCause();
}
default -> {
throw ExceptionBox.toUnchecked(ExceptionBox.unbox(cause));
}
}
}
final int exitCode = process.waitFor();
trace("Done. Exit code: " + exitCode);
return createResult(exitCode, output);
return execute(sb, commandOutputControl.createExecutable(builder));
}
private int runToolProvider(PrintStream out, PrintStream err) {
private Result runToolProvider() throws IOException, InterruptedException {
final var sb = new StringBuilder(getPrintableCommandLine());
outputStreamsControl.describe().ifPresent(desc -> {
sb.append("; ").append(desc);
});
trace("Execute " + sb + "...");
final int exitCode = toolProvider.run(out, err, args.toArray(
String[]::new));
trace("Done. Exit code: " + exitCode);
return exitCode;
sb.append("; ").append(commandOutputControl.description());
return execute(sb, commandOutputControl.createExecutable(toolProvider, args.toArray(String[]::new)));
}
private Result runToolProvider() throws IOException {
final var toolProviderStreamConfig = ToolProviderStreamConfig.create(outputStreamsControl);
private Result execute(StringBuilder traceMsg, CommandOutputControl.Executable exec) throws IOException, InterruptedException {
Objects.requireNonNull(traceMsg);
final var exitCode = runToolProvider(toolProviderStreamConfig);
trace("Execute " + traceMsg + "...");
final var output = combine(
read(outputStreamsControl.stdout(), toolProviderStreamConfig.out()),
read(outputStreamsControl.stderr(), toolProviderStreamConfig.err()));
return createResult(exitCode, output);
var result = exec.execute();
trace("Done. Exit code: " + result.getExitCode());
return createResult(result);
}
private int runToolProvider(ToolProviderStreamConfig cfg) throws IOException {
try {
return runToolProvider(cfg.out().ps(), cfg.err().ps());
} finally {
cfg.out().ps().flush();
cfg.err().ps().flush();
}
}
private static Optional<List<String>> processProcessStream(OutputControl outputControl, InputStream in) throws IOException {
List<String> outputLines = null;
try (final var bufReader = new BufferedReader(new InputStreamReader(in))) {
if (outputControl.dump() || outputControl.saveAll()) {
outputLines = bufReader.lines().toList();
} else if (outputControl.saveFirstLine()) {
outputLines = Optional.ofNullable(bufReader.readLine()).map(List::of).orElseGet(List::of);
// Read all input, or the started process may exit with an error (cmd.exe does so).
bufReader.transferTo(Writer.nullWriter());
} else {
// This should be empty input stream, fetch it anyway.
bufReader.transferTo(Writer.nullWriter());
}
} finally {
if (outputControl.dump() && outputLines != null) {
outputLines.forEach(System.out::println);
if (outputControl.saveFirstLine()) {
outputLines = outputLines.stream().findFirst().map(List::of).orElseGet(List::of);
}
}
if (!outputControl.save()) {
outputLines = null;
}
}
return Optional.ofNullable(outputLines);
}
private static Optional<List<String>> read(OutputControl outputControl, CachingPrintStream cps) throws IOException {
final var bufferAsString = cps.bufferContents();
try (final var bufReader = new BufferedReader(new StringReader(bufferAsString.orElse("")))) {
if (outputControl.saveFirstLine()) {
return Optional.of(bufReader.lines().findFirst().map(List::of).orElseGet(List::of));
} else if (outputControl.saveAll()) {
return Optional.of(bufReader.lines().toList());
} else if (bufferAsString.isPresent()) {
return Optional.of(List.of());
} else {
return Optional.empty();
}
}
}
private CommandOutput combine(Optional<List<String>> out, Optional<List<String>> err) {
if (out.isEmpty() && err.isEmpty()) {
return new CommandOutput();
} else if (out.isEmpty()) {
return new CommandOutput(err, -1);
} else if (err.isEmpty()) {
return new CommandOutput(out, Integer.MAX_VALUE);
} else {
final var combined = Stream.of(out, err).map(Optional::orElseThrow).flatMap(List::stream);
if (outputStreamsControl.stdout().saveFirstLine() && outputStreamsControl.stderr().saveFirstLine()) {
return new CommandOutput(Optional.of(combined.findFirst().map(List::of).orElseGet(List::of)),
Integer.min(1, out.orElseThrow().size()));
} else {
return new CommandOutput(Optional.of(combined.toList()), out.orElseThrow().size());
}
}
}
private Result createResult(int exitCode, CommandOutput output) {
return new Result(exitCode, output, this::getPrintableCommandLine);
private Result createResult(CommandOutputControl.Result baseResult) {
return new Result(baseResult.copyWithExecutableAttributes(
new ExecutableAttributes(baseResult.execAttrs(), getPrintableCommandLine())));
}
public String getPrintableCommandLine() {
@ -618,359 +525,40 @@ public final class Executor extends CommandArguments<Executor> {
var cmdline = Stream.of(prefixCommandLineArgs(), List.of(exec), args).flatMap(
List::stream).toList();
return String.format(format, printCommandLine(cmdline), cmdline.size());
return String.format(format, CommandLineFormat.DEFAULT.apply(cmdline), cmdline.size());
}
private static String printCommandLine(List<String> cmdline) {
// Want command line printed in a way it can be easily copy/pasted
// to be executed manually
Pattern regex = Pattern.compile("\\s");
return cmdline.stream().map(
v -> (v.isEmpty() || regex.matcher(v).find()) ? "\"" + v + "\"" : v).collect(
Collectors.joining(" "));
private record ExecutableAttributes(CommandOutputControl.ExecutableAttributes base, String toStringValue)
implements CommandOutputControl.ExecutableAttributes {
ExecutableAttributes {
Objects.requireNonNull(base);
if (toStringValue.isBlank()) {
throw new IllegalArgumentException();
}
}
@Override
public String toString() {
return toStringValue;
}
@Override
public List<String> commandLine() {
return base.commandLine();
}
}
private static void trace(String msg) {
TKit.trace(String.format("exec: %s", msg));
}
private static PrintStream nullPrintStream() {
return new PrintStream(OutputStream.nullOutputStream());
}
private record OutputStreamsControl(OutputControl stdout, OutputControl stderr) {
OutputStreamsControl {
Objects.requireNonNull(stdout);
Objects.requireNonNull(stderr);
}
OutputStreamsControl() {
this(new OutputControl(), new OutputControl());
}
void applyTo(ProcessBuilder pb) {
pb.redirectOutput(stdout.asProcessBuilderRedirect());
pb.redirectError(stderr.asProcessBuilderRedirect());
}
Optional<String> describe() {
final List<String> tokens = new ArrayList<>();
if (stdout.save() || stderr.save()) {
streamsLabel("save ", true).ifPresent(tokens::add);
}
if (stdout.dump() || stderr.dump()) {
streamsLabel("inherit ", true).ifPresent(tokens::add);
}
streamsLabel("discard ", false).ifPresent(tokens::add);
if (tokens.isEmpty()) {
return Optional.empty();
} else {
return Optional.of(String.join("; ", tokens));
}
}
Optional<String> streamsLabel(String prefix, boolean negate) {
Objects.requireNonNull(prefix);
final var str = Stream.of(stdoutLabel(negate), stderrLabel(negate))
.filter(Optional::isPresent)
.map(Optional::orElseThrow)
.collect(joining("+"));
if (str.isEmpty()) {
return Optional.empty();
} else {
return Optional.of(prefix + str);
}
}
private Optional<String> stdoutLabel(boolean negate) {
if ((stdout.discard() && !negate) || (!stdout.discard() && negate)) {
return Optional.of("out");
} else {
return Optional.empty();
}
}
private Optional<String> stderrLabel(boolean negate) {
if ((stderr.discard() && !negate) || (!stderr.discard() && negate)) {
return Optional.of("err");
} else {
return Optional.empty();
}
}
}
private record CachingPrintStream(PrintStream ps, Optional<ByteArrayOutputStream> buf) {
CachingPrintStream {
Objects.requireNonNull(ps);
Objects.requireNonNull(buf);
}
Optional<String> bufferContents() {
return buf.map(ByteArrayOutputStream::toString);
}
static Builder build() {
return new Builder();
}
static final class Builder {
Builder save(boolean v) {
save = v;
return this;
}
Builder discard(boolean v) {
discard = v;
return this;
}
Builder dumpStream(PrintStream v) {
dumpStream = v;
return this;
}
CachingPrintStream create() {
final Optional<ByteArrayOutputStream> buf;
if (save && !discard) {
buf = Optional.of(new ByteArrayOutputStream());
} else {
buf = Optional.empty();
}
final PrintStream ps;
if (buf.isPresent() && dumpStream != null) {
ps = new PrintStream(new TeeOutputStream(List.of(buf.orElseThrow(), dumpStream)), true, dumpStream.charset());
} else if (!discard) {
ps = buf.map(PrintStream::new).or(() -> Optional.ofNullable(dumpStream)).orElseGet(Executor::nullPrintStream);
} else {
ps = nullPrintStream();
}
return new CachingPrintStream(ps, buf);
}
private boolean save;
private boolean discard;
private PrintStream dumpStream;
}
}
private record ToolProviderStreamConfig(CachingPrintStream out, CachingPrintStream err) {
ToolProviderStreamConfig {
Objects.requireNonNull(out);
Objects.requireNonNull(err);
}
static ToolProviderStreamConfig create(OutputStreamsControl cfg) {
final var errCfgBuilder = cfg.stderr().buildCachingPrintStream(System.err);
if (cfg.stderr().dump() && cfg.stderr().save()) {
errCfgBuilder.dumpStream(System.out);
}
return new ToolProviderStreamConfig(
cfg.stdout().buildCachingPrintStream(System.out).create(), errCfgBuilder.create());
}
}
private static final class OutputControl {
boolean save() {
return save.isPresent();
}
boolean saveAll() {
return save.orElse(null) == OutputControlOption.SAVE_ALL;
}
boolean saveFirstLine() {
return save.orElse(null) == OutputControlOption.SAVE_FIRST_LINE;
}
boolean discard() {
return discard || (!dump && save.isEmpty());
}
boolean dump() {
return !discard && dump;
}
OutputControl dump(boolean v) {
this.dump = v;
return this;
}
OutputControl discard(boolean v) {
this.discard = v;
return this;
}
OutputControl saveAll(boolean v) {
if (v) {
save = Optional.of(OutputControlOption.SAVE_ALL);
} else {
save = Optional.empty();
}
return this;
}
OutputControl saveFirstLine(boolean v) {
if (v) {
save = Optional.of(OutputControlOption.SAVE_FIRST_LINE);
} else {
save = Optional.empty();
}
return this;
}
OutputControl set(boolean set, OutputControlOption v) {
switch (v) {
case DUMP -> dump(set);
case SAVE_ALL -> saveAll(set);
case SAVE_FIRST_LINE -> saveFirstLine(set);
}
return this;
}
ProcessBuilder.Redirect asProcessBuilderRedirect() {
if (discard()) {
return ProcessBuilder.Redirect.DISCARD;
} else if (dump && !save()) {
return ProcessBuilder.Redirect.INHERIT;
} else {
return ProcessBuilder.Redirect.PIPE;
}
}
CachingPrintStream.Builder buildCachingPrintStream(PrintStream dumpStream) {
Objects.requireNonNull(dumpStream);
final var builder = CachingPrintStream.build().save(save()).discard(discard());
if (dump()) {
builder.dumpStream(dumpStream);
}
return builder;
}
private boolean dump;
private boolean discard;
private Optional<OutputControlOption> save = Optional.empty();
}
private static final class TeeOutputStream extends OutputStream {
public TeeOutputStream(Iterable<OutputStream> streams) {
streams.forEach(Objects::requireNonNull);
this.streams = streams;
}
@Override
public void write(int b) throws IOException {
for (final var out : streams) {
out.write(b);
}
}
@Override
public void write(byte[] b) throws IOException {
for (final var out : streams) {
out.write(b);
}
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
for (final var out : streams) {
out.write(b, off, len);
}
}
@Override
public void flush() throws IOException {
forEach(OutputStream::flush);
}
@Override
public void close() throws IOException {
forEach(OutputStream::close);
}
private void forEach(OutputStreamConsumer c) throws IOException {
IOException firstEx = null;
for (final var out : streams) {
try {
c.accept(out);
} catch (IOException e) {
if (firstEx == null) {
firstEx = e;
}
}
}
if (firstEx != null) {
throw firstEx;
}
}
@FunctionalInterface
private static interface OutputStreamConsumer {
void accept(OutputStream out) throws IOException;
}
private final Iterable<OutputStream> streams;
}
private static final class CommandOutput {
CommandOutput(Optional<List<String>> lines, int stdoutLineCount) {
this.lines = Objects.requireNonNull(lines);
this.stdoutLineCount = stdoutLineCount;
}
CommandOutput() {
this(Optional.empty(), 0);
}
Optional<List<String>> lines() {
return lines;
}
Optional<List<String>> stdoutLines() {
if (lines.isEmpty() || stdoutLineCount < 0) {
return Optional.empty();
}
final var theLines = lines.orElseThrow();
if (stdoutLineCount == theLines.size()) {
return lines;
} else {
return Optional.of(theLines.subList(0, Integer.min(stdoutLineCount, theLines.size())));
}
}
Optional<List<String>> stderrLines() {
if (lines.isEmpty() || stdoutLineCount > lines.orElseThrow().size()) {
return Optional.empty();
} else if (stdoutLineCount == 0) {
return lines;
} else {
final var theLines = lines.orElseThrow();
return Optional.of(theLines.subList(stdoutLineCount, theLines.size()));
}
}
private final Optional<List<String>> lines;
private final int stdoutLineCount;
static final CommandOutput EMPTY = new CommandOutput();
}
private ToolProvider toolProvider;
private Path executable;
private OutputStreamsControl outputStreamsControl;
private final CommandOutputControl commandOutputControl = new CommandOutputControl();
private Path directory;
private Set<String> removeEnvVars = new HashSet<>();
private Map<String, String> setEnvVars = new HashMap<>();
private boolean winEnglishOutput;
private String winTmpDir = null;
private static enum OutputControlOption {
SAVE_ALL, SAVE_FIRST_LINE, DUMP
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2026, 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,8 @@ import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
@ -780,10 +782,9 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
}
/**
* Starts a new thread. In this thread calls
* {@link #useToolProviderByDefault(ToolProvider)} with the specified
* {@code jpackageToolProvider} and then calls {@code workload.run()}. Joins the
* thread.
* In a separate thread calls {@link #useToolProviderByDefault(ToolProvider)}
* with the specified {@code jpackageToolProvider} and then calls
* {@code workload.run()}. Joins the thread.
* <p>
* The idea is to run the {@code workload} in the context of the specified
* jpackage {@code ToolProvider} without altering the global variable holding
@ -794,13 +795,23 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
* @param jpackageToolProvider jpackage {@code ToolProvider}
* @param workload the workload to run
*/
public static void withToolProvider(ToolProvider jpackageToolProvider, Runnable workload) {
Objects.requireNonNull(jpackageToolProvider);
public static void withToolProvider(Runnable workload, ToolProvider jpackageToolProvider) {
Objects.requireNonNull(workload);
ThrowingRunnable.toRunnable(Thread.ofVirtual().start(() -> {
Objects.requireNonNull(jpackageToolProvider);
CompletableFuture.runAsync(() -> {
var oldValue = defaultToolProvider.get();
useToolProviderByDefault(jpackageToolProvider);
workload.run();
})::join).run();
try {
workload.run();
} finally {
defaultToolProvider.set(oldValue);
}
// Run the future in a new native thread. Don't run it in a virtual/pooled thread.
// Pooled and/or virtual threads are problematic when used with inheritable thread-local variables.
// TKit class depends on such a variable, which results in intermittent test failures
// if the default executor runs this future.
}, Executors.newThreadPerTaskExecutor(Thread.ofPlatform().factory())).join();
}
public JPackageCommand useToolProvider(boolean v) {
@ -1022,7 +1033,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
outputValidator.accept(result.getOutput().iterator());
}
if (result.exitCode() == 0 && expectedExitCode.isPresent()) {
if (result.getExitCode() == 0 && expectedExitCode.isPresent()) {
verifyActions.run();
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2026, 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
@ -723,7 +723,9 @@ public final class LinuxHelper {
private static Optional<String> queryMimeTypeDefaultHandler(String mimeType) {
return Executor.of("xdg-mime", "query", "default", mimeType)
.discardStderr().saveFirstLineOfOutput().execute().findFirstLineOfOutput();
.discardStderr()
.saveFirstLineOfOutput()
.execute().getOutput().stream().findFirst();
}
private static void verifyIconInScriptlet(Scriptlet scriptletType,

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2026, 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,11 +49,11 @@ import java.util.Collections;
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.Properties;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
@ -66,10 +66,10 @@ import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import jdk.jpackage.internal.RetryExecutor;
import jdk.jpackage.internal.util.FileUtils;
import jdk.jpackage.internal.util.PListReader;
import jdk.jpackage.internal.util.PathUtils;
import jdk.jpackage.internal.util.RetryExecutor;
import jdk.jpackage.internal.util.XmlUtils;
import jdk.jpackage.internal.util.function.ThrowingConsumer;
import jdk.jpackage.internal.util.function.ThrowingSupplier;
@ -90,38 +90,34 @@ public final class MacHelper {
final var mountRoot = TKit.createTempDirectory("mountRoot");
// Explode DMG assuming this can require interaction, thus use `yes`.
String attachCMD[] = {
"sh", "-c",
String.join(" ", "yes", "|", "/usr/bin/hdiutil", "attach",
JPackageCommand.escapeAndJoin(cmd.outputBundle().toString()),
"-mountroot", PathUtils.normalizedAbsolutePathString(mountRoot),
"-nobrowse", "-plist")};
RetryExecutor attachExecutor = new RetryExecutor();
try {
// 10 times with 6 second delays.
attachExecutor.setMaxAttemptsCount(10)
.setAttemptTimeoutMillis(6000)
.setWriteOutputToFile(true)
.saveOutput(true)
.execute(attachCMD);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
final var attachStdout = Executor.of("sh", "-c", String.join(" ",
"yes",
"|",
"/usr/bin/hdiutil",
"attach",
JPackageCommand.escapeAndJoin(cmd.outputBundle().toString()),
"-mountroot", PathUtils.normalizedAbsolutePathString(mountRoot),
"-nobrowse",
"-plist"
)).saveOutput().storeOutputInFiles().executeAndRepeatUntilExitCode(0, 10, 6).stdout();
Path mountPoint = null;
final Path mountPoint;
boolean mountPointInitialized = false;
try {
// One of "dict" items of "system-entities" array property should contain "mount-point" string property.
mountPoint = readPList(attachExecutor.getOutput()).queryArrayValue("system-entities", false).map(PListReader.class::cast).map(dict -> {
try {
return dict.queryValue("mount-point");
} catch (NoSuchElementException ex) {
return (String)null;
}
}).filter(Objects::nonNull).map(Path::of).findFirst().orElseThrow();
mountPoint = readPList(attachStdout).queryArrayValue("system-entities", false)
.map(PListReader.class::cast)
.map(dict -> {
return dict.findValue("mount-point");
})
.filter(Optional::isPresent).map(Optional::get)
.map(Path::of).findFirst().orElseThrow();
mountPointInitialized = true;
} finally {
if (mountPoint == null) {
if (!mountPointInitialized) {
TKit.trace("Unexpected plist file missing `system-entities` array:");
attachExecutor.getOutput().forEach(TKit::trace);
attachStdout.forEach(TKit::trace);
TKit.trace("Done");
}
}
@ -138,39 +134,27 @@ public final class MacHelper {
ThrowingConsumer.toConsumer(consumer).accept(childPath);
}
} finally {
String detachCMD[] = {
"/usr/bin/hdiutil",
"detach",
"-verbose",
mountPoint.toAbsolutePath().toString()};
// "hdiutil detach" might not work right away due to resource busy error, so
// repeat detach several times.
RetryExecutor detachExecutor = new RetryExecutor();
// Image can get detach even if we got resource busy error, so stop
// trying to detach it if it is no longer attached.
final Path mp = mountPoint;
detachExecutor.setExecutorInitializer(exec -> {
if (!Files.exists(mp)) {
detachExecutor.abort();
new RetryExecutor<Void, RuntimeException>(RuntimeException.class).setExecutable(context -> {
var exec = Executor.of("/usr/bin/hdiutil", "detach").storeOutputInFiles();
if (context.isLastAttempt()) {
// The last attempt, force detach.
exec.addArgument("-force");
}
});
try {
// 10 times with 6 second delays.
detachExecutor.setMaxAttemptsCount(10)
.setAttemptTimeoutMillis(6000)
.setWriteOutputToFile(true)
.saveOutput(true)
.execute(detachCMD);
} catch (IOException ex) {
if (!detachExecutor.isAborted()) {
// Now force to detach if it still attached
if (Files.exists(mountPoint)) {
Executor.of("/usr/bin/hdiutil", "detach",
"-force", "-verbose")
.addArgument(mountPoint).execute();
}
exec.addArgument(mountPoint);
// The image can get detached even if we get a resource busy error,
// so execute the detach command without checking the exit code.
var result = exec.executeWithoutExitCodeCheck();
if (result.getExitCode() == 0 || !Files.exists(mountPoint)) {
// Detached successfully!
return null;
} else {
throw new RuntimeException(String.format("[%s] mount point still attached", mountPoint));
}
}
}).setMaxAttemptsCount(10).setAttemptTimeout(6, TimeUnit.SECONDS).execute();
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -1172,7 +1172,7 @@ public final class MacSign {
"-c", certFile.normalize().toString(),
"-k", keychain.name(),
"-p", resolvedCertificateRequest.installed().type().verifyPolicy()).saveOutput(!quite).executeWithoutExitCodeCheck();
if (result.exitCode() == 0) {
if (result.getExitCode() == 0) {
return VerifyStatus.VERIFY_OK;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -94,7 +94,7 @@ public final class MacSignVerify {
public static Optional<PListReader> findEntitlements(Path path) {
final var exec = Executor.of("/usr/bin/codesign", "-d", "--entitlements", "-", "--xml", path.toString()).saveOutput().dumpOutput();
final var result = exec.execute();
var xml = result.stdout().getOutput();
var xml = result.stdout();
if (xml.isEmpty()) {
return Optional.empty();
} else {
@ -137,7 +137,7 @@ public final class MacSignVerify {
public static Optional<String> findSpctlSignOrigin(SpctlType type, Path path) {
final var exec = Executor.of("/usr/sbin/spctl", "-vv", "--raw", "--assess", "--type", type.value(), path.toString()).saveOutput().discardStderr();
final var result = exec.executeWithoutExitCodeCheck();
TKit.assertTrue(Set.of(0, 3).contains(result.exitCode()),
TKit.assertTrue(Set.of(0, 3).contains(result.getExitCode()),
String.format("Check exit code of command %s is either 0 or 3", exec.getPrintableCommandLine()));
return toSupplier(() -> {
try {
@ -173,7 +173,7 @@ public final class MacSignVerify {
} else if (result.getExitCode() == 1 && result.getFirstLineOfOutput().endsWith("code object is not signed at all")) {
return Optional.empty();
} else {
reportUnexpectedCommandOutcome(exec.getPrintableCommandLine(), result);
reportUnexpectedCommandOutcome(result);
return null; // Unreachable
}
}
@ -205,7 +205,7 @@ public final class MacSignVerify {
TKit.trace("Try /usr/bin/codesign again with `sudo`");
assertSigned(path, true);
} else {
reportUnexpectedCommandOutcome(exec.getPrintableCommandLine(), result);
reportUnexpectedCommandOutcome(result);
}
}
@ -264,13 +264,13 @@ public final class MacSignVerify {
return signIdentities;
} catch (Exception ex) {
ex.printStackTrace();
reportUnexpectedCommandOutcome(exec.getPrintableCommandLine(), result);
reportUnexpectedCommandOutcome(result);
return null; // Unreachable
}
} else if (result.getExitCode() == 1 && result.getOutput().getLast().endsWith("Status: no signature")) {
return List.of();
} else {
reportUnexpectedCommandOutcome(exec.getPrintableCommandLine(), result);
reportUnexpectedCommandOutcome(result);
return null; // Unreachable
}
}
@ -282,14 +282,13 @@ public final class MacSignVerify {
}
}
private static void reportUnexpectedCommandOutcome(String printableCommandLine, Executor.Result result) {
Objects.requireNonNull(printableCommandLine);
private static void reportUnexpectedCommandOutcome(Executor.Result result) {
Objects.requireNonNull(result);
TKit.trace(String.format("Command %s exited with exit code %d and the following output:",
printableCommandLine, result.getExitCode()));
result.getPrintableCommandLine(), result.getExitCode()));
result.getOutput().forEach(TKit::trace);
TKit.trace("Done");
TKit.assertUnexpected(String.format("Outcome of command %s", printableCommandLine));
TKit.assertUnexpected(String.format("Outcome of command %s", result.getPrintableCommandLine()));
}
private static final Pattern SIGN_IDENTITY_NAME_REGEXP = Pattern.compile("^\\s+\\d+\\.\\s+(.*)$");

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2026, 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
@ -81,16 +81,16 @@ public class WindowsHelper {
msiLog.ifPresent(v -> misexec.clearArguments().addArguments(origArgs).addArgument("/L*v").addArgument(v));
result = misexec.executeWithoutExitCodeCheck();
if (result.exitCode() == 1605) {
if (result.getExitCode() == 1605) {
// ERROR_UNKNOWN_PRODUCT, attempt to uninstall not installed
// package
return result.exitCode();
return result.getExitCode();
}
// 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 && isUnpack)) {
if ((result.getExitCode() == 1618) || (result.getExitCode() == 1603 && isUnpack)) {
// Another installation is already in progress.
// Wait a little and try again.
Long timeout = 1000L * (attempt + 3); // from 3 to 10 seconds
@ -100,7 +100,7 @@ public class WindowsHelper {
break;
}
return result.exitCode();
return result.getExitCode();
}
static PackageHandlers createMsiPackageHandlers(boolean createMsiLog) {
@ -462,7 +462,7 @@ public class WindowsHelper {
var status = Executor.of("reg", "query", keyPath, "/v", valueName)
.saveOutput()
.executeWithoutExitCodeCheck();
if (status.exitCode() == 1) {
if (status.getExitCode() == 1) {
// Should be the case of no such registry value or key
String lookupString = "ERROR: The system was unable to find the specified registry key or value.";
TKit.assertTextStream(lookupString)

View File

@ -0,0 +1,76 @@
/*
* Copyright (c) 2026, 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.mock;
import java.io.PrintStream;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* An action.
*/
@FunctionalInterface
public interface CommandAction {
public record Context(PrintStream out, PrintStream err, List<String> args) {
public Context {
Objects.requireNonNull(out);
Objects.requireNonNull(err);
args.forEach(Objects::requireNonNull);
}
public Optional<String> findOptionValue(String option) {
Objects.requireNonNull(option);
var idx = args.indexOf(option);
if (idx >= 0 && idx + 1 < args.size()) {
return Optional.of(args.get(idx + 1));
} else {
return Optional.empty();
}
}
public String optionValue(String option) {
return findOptionValue(option).orElseThrow(() -> {
throw new MockIllegalStateException(String.format("No option %s", option));
});
}
public MockIllegalStateException unexpectedArguments() {
return new MockIllegalStateException(String.format("Unexpected arguments: %s", args));
}
}
/**
* Runs the action in the given context.
*
* @param context the context
* @return an {@code Optional} wrapping the exit code, indicating it is the last
* action in the sequence or an empty {@code Optional} otherwise
* @throws Exception simulates a failure
* @throws MockIllegalStateException if error in internal mock logic occurred.
* E.g.: if the action was called unexpectedly
*/
Optional<Integer> run(Context context) throws Exception, MockIllegalStateException;
}

View File

@ -0,0 +1,86 @@
/*
* Copyright (c) 2026, 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.mock;
import java.util.Objects;
import java.util.Optional;
import jdk.jpackage.internal.util.function.ThrowingSupplier;
import jdk.jpackage.internal.util.function.ThrowingConsumer;
import jdk.jpackage.internal.util.function.ThrowingRunnable;
/**
* Specification of a {@link CommandAction}.
* <p>
* Comprised of a human-readable description and an associated action.
*/
public interface CommandActionSpec {
String description();
CommandAction action();
public static CommandActionSpec create(String description, CommandAction action) {
return new Internal.DefaultCommandActionSpec(description, action);
}
public static CommandActionSpec create(String description, ThrowingSupplier<Integer, Exception> action) {
Objects.requireNonNull(action);
return create(description, _ -> {
return Optional.of(action.get());
});
}
public static CommandActionSpec create(String description, ThrowingRunnable<Exception> action) {
Objects.requireNonNull(action);
return create(description, _ -> {
action.run();
return Optional.empty();
});
}
@SuppressWarnings("overloads")
public static CommandActionSpec create(String description, ThrowingConsumer<CommandAction.Context, Exception> action) {
Objects.requireNonNull(action);
return create(description, context -> {
action.accept(context);
return Optional.empty();
});
}
final class Internal {
private Internal() {
}
private record DefaultCommandActionSpec(String description, CommandAction action) implements CommandActionSpec {
DefaultCommandActionSpec {
Objects.requireNonNull(description);
Objects.requireNonNull(action);
}
@Override
public String toString() {
return description();
}
}
}
}

View File

@ -0,0 +1,185 @@
/*
* Copyright (c) 2026, 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.mock;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Stream;
import jdk.jpackage.internal.util.function.ExceptionBox;
/**
* A sequence of actions.
*/
public record CommandActionSpecs(List<CommandActionSpec> specs) {
public CommandActionSpecs {
Objects.requireNonNull(specs);
}
public CommandActionSpecs andThen(CommandActionSpecs other) {
return build().append(this).append(other).create();
}
public Stream<CommandAction> actions() {
return specs.stream().map(CommandActionSpec::action);
}
public CommandMock.Builder toCommandMockBuilder() {
return new CommandMock.Builder().mutate(builder -> {
builder.actions().append(this);
});
}
@Override
public String toString() {
return specs.toString();
}
public static Builder build() {
return new Builder();
}
public static final class Builder {
public CommandActionSpecs create() {
return new CommandActionSpecs(List.copyOf(specs));
}
public Builder stdout(List<String> content) {
Objects.requireNonNull(content);
return action(CommandActionSpec.create(String.format("%s>>1", content), context -> {
var out = context.out();
content.forEach(out::println);
}));
}
public Builder stdout(String... str) {
return stdout(List.of(str));
}
public Builder stderr(List<String> content) {
Objects.requireNonNull(content);
return action(CommandActionSpec.create(String.format("%s>>2", content), context -> {
var err = context.err();
content.forEach(err::println);
}));
}
public Builder stderr(String... str) {
return stderr(List.of(str));
}
public Builder printToStdout(List<String> content) {
Objects.requireNonNull(content);
return action(CommandActionSpec.create(String.format("%s(no-eol)>>1", content), context -> {
var out = context.out();
content.forEach(out::print);
}));
}
public Builder printToStdout(String... str) {
return printToStdout(List.of(str));
}
public Builder printToStderr(List<String> content) {
Objects.requireNonNull(content);
return action(CommandActionSpec.create(String.format("%s(no-eol)>>2", content), context -> {
var err = context.err();
content.forEach(err::print);
}));
}
public Builder printToStderr(String... str) {
return printToStderr(List.of(str));
}
public Builder exit(int exitCode) {
return action(CommandActionSpec.create(String.format("exit(%d)", exitCode), () -> {
return exitCode;
}));
}
public Builder exit() {
return exit(0);
}
public Builder exit(CommandMockExit exit) {
switch (exit) {
case SUCCEED -> {
return exit();
}
case EXIT_1 -> {
return exit(1);
}
case THROW_MOCK_IO_EXCEPTION -> {
return action(CommandActionSpec.create("<I/O error>", () -> {
throw new MockingToolProvider.RethrowableException(new MockIOException("Kaput!"));
}));
}
default -> {
throw ExceptionBox.reachedUnreachable();
}
}
}
public Builder mutate(Consumer<Builder> mutator) {
mutator.accept(this);
return this;
}
public Builder append(Builder other) {
return append(other.specs);
}
public Builder append(CommandActionSpecs other) {
return append(other.specs());
}
public Builder append(List<CommandActionSpec> other) {
specs.addAll(other);
return this;
}
public Builder action(CommandActionSpec v) {
specs.add(Objects.requireNonNull(v));
return this;
}
public Builder copy() {
return new Builder().append(this);
}
public CommandMock.Builder toCommandMockBuilder() {
return new CommandMock.Builder().mutate(builder -> {
builder.actions(this);
});
}
private final List<CommandActionSpec> specs = new ArrayList<>();
}
public static final CommandActionSpecs UNREACHABLE = new CommandActionSpecs(List.of());
}

View File

@ -0,0 +1,128 @@
/*
* Copyright (c) 2026, 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.mock;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.IntStream;
/**
* Command mock.
*/
public sealed interface CommandMock permits ToolProviderCommandMock, VerbatimCommandMock, CompletableCommandMock {
public static CommandMock ioerror(String name) {
return CommandActionSpecs.build()
.exit(CommandMockExit.THROW_MOCK_IO_EXCEPTION)
.toCommandMockBuilder().name(Objects.requireNonNull(name)).create();
}
public static CommandMock fail(String name) {
return CommandActionSpecs.build()
.exit(CommandMockExit.EXIT_1)
.toCommandMockBuilder().name(Objects.requireNonNull(name)).create();
}
public static CommandMock succeed(String name) {
return CommandActionSpecs.build()
.exit(CommandMockExit.SUCCEED)
.toCommandMockBuilder().name(Objects.requireNonNull(name)).create();
}
public static CommandMock unreachable() {
return MockingToolProvider.UNREACHABLE;
}
public final class Builder {
public ToolProviderCommandMock create() {
var actionSpecs = Optional.ofNullable(scriptBuilder)
.map(CommandActionSpecs.Builder::create)
.orElse(CommandActionSpecs.UNREACHABLE);
if (actionSpecs.equals(CommandActionSpecs.UNREACHABLE)) {
return (ToolProviderCommandMock)unreachable();
}
var theName = Optional.ofNullable(name).orElse("mock");
var script = actionSpecs.actions().toList();
switch (repeat) {
case 0 -> {
return MockingToolProvider.create(theName, script);
}
case -1 -> {
return MockingToolProvider.createLoop(theName, script);
}
default -> {
var repeatedScript = IntStream.rangeClosed(0, repeat)
.mapToObj(i -> script)
.flatMap(List::stream)
.toList();
return MockingToolProvider.create(theName, repeatedScript);
}
}
}
public Builder name(String v) {
name = v;
return this;
}
public Builder mutate(Consumer<Builder> mutator) {
mutator.accept(this);
return this;
}
public Builder repeat(int v) {
repeat = Integer.max(-1, v);
return this;
}
public Builder noRepeats() {
return repeat(0);
}
public Builder repeatInfinitely() {
return repeat(-1);
}
public Builder actions(CommandActionSpecs.Builder v) {
scriptBuilder = Optional.ofNullable(v).orElseGet(CommandActionSpecs::build);
return this;
}
public CommandActionSpecs.Builder actions() {
if (scriptBuilder == null) {
scriptBuilder = CommandActionSpecs.build();
}
return scriptBuilder;
}
private String name;
private int repeat = -1;
private CommandActionSpecs.Builder scriptBuilder;
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright (c) 2026, 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.mock;
import jdk.jpackage.internal.util.CommandOutputControl;
public enum CommandMockExit {
/**
* Exit normally with "0" exit code.
*/
SUCCEED(true, true),
/**
* Exit normally with "1" exit code.
*/
EXIT_1(false, true),
/**
* Throw {@link MockIOException}. This simulates a situation when an I/O error
* occurs starting a subprocess with {@link ProcessBuilder#start()}.
* {@link CommandOutputControl.Executable#execute()} will handle I/O errors and
* let them out.
*/
THROW_MOCK_IO_EXCEPTION(false, false),
;
CommandMockExit(boolean succeed, boolean exitNormally) {
this.succeed = succeed;
this.exitNormally = exitNormally;
}
public boolean succeed() {
return succeed;
}
public boolean exitNormally() {
return exitNormally;
}
private final boolean succeed;
private final boolean exitNormally;
}

View File

@ -0,0 +1,63 @@
/*
* Copyright (c) 2026, 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.mock;
import java.nio.file.Path;
import java.util.Objects;
/**
* Specification of a {@link CommandMock}.
*/
public record CommandMockSpec(Path name, Path mockName, CommandActionSpecs actions) {
public CommandMockSpec {
Objects.requireNonNull(name);
Objects.requireNonNull(mockName);
Objects.requireNonNull(actions);
}
public CommandMockSpec(Path name, CommandActionSpecs actions) {
this(name, Path.of(name.toString() + "-mock"), actions);
}
public CommandMockSpec(String name, CommandActionSpecs actions) {
this(Path.of(name), actions);
}
public CommandMockSpec(String name, String mockName, CommandActionSpecs actions) {
this(Path.of(name), Path.of(mockName), actions);
}
public CommandMock.Builder toCommandMockBuilder() {
return actions.toCommandMockBuilder().name(mockName.toString());
}
public boolean isDefaultMockName() {
return (name.getFileName().toString() + "-mock").equals(mockName.getFileName().toString());
}
@Override
public String toString() {
return String.format("mock-of(%s)%s", name, actions);
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2026, 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.mock;
/**
* Command mock that runs a finite sequence of actions.
*/
public sealed interface CompletableCommandMock extends CommandMock permits ToolProviderCompletableCommandMock {
boolean completed();
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2026, 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.mock;
import java.io.IOException;
/**
* Simulates I/O error.
*
* @see CommandMockExit#THROW_MOCK_IO_EXCEPTION
*/
public final class MockIOException extends IOException {
MockIOException(String msg) {
super(msg);
}
private static final long serialVersionUID = 1L;
}

View File

@ -0,0 +1,35 @@
/*
* Copyright (c) 2026, 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.mock;
/**
* Indicates command mock internal error.
*/
public final class MockIllegalStateException extends IllegalStateException {
public MockIllegalStateException(String msg) {
super(msg);
}
private static final long serialVersionUID = 1L;
}

View File

@ -0,0 +1,164 @@
/*
* Copyright (c) 2026, 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.mock;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import jdk.jpackage.internal.util.function.ExceptionBox;
/**
* A command simulator implementing {@code ToolProvider}.
* <p>
* Iterates over actions and runs them. Each action is write to stdout/stderr, create a file, etc.
*/
abstract sealed class MockingToolProvider implements ToolProviderCommandMock {
MockingToolProvider(String name, Iterator<CommandAction> actionIter) {
this.name = Objects.requireNonNull(name);
this.actionIter = Objects.requireNonNull(actionIter);
}
static ToolProviderCommandMock createLoop(String name, Iterable<CommandAction> actions) {
return new MockingToolProvider.NonCompletable(name, actions);
}
static MockingToolProvider create(String name, Iterable<CommandAction> actions) {
return new MockingToolProvider.Completable(name, actions);
}
public boolean completed() {
return !actionIter.hasNext();
}
@Override
public String name() {
return name;
}
@Override
public int run(PrintStream out, PrintStream err, String... args) {
var context = new CommandAction.Context(out, err, List.of(args));
try {
while (actionIter.hasNext()) {
var action = actionIter.next();
var reply = action.run(context);
if (reply.isPresent()) {
return reply.get();
}
}
} catch (RethrowableException ex) {
// Let the checked exception out.
throwAny(ex.getCause());
// Unreachable
return 0;
} catch (Exception ex) {
throw ExceptionBox.toUnchecked(ex);
}
// No more actions to execute, but still expect it to keep going.
throw new MockIllegalStateException("No more actions to execute");
}
@Override
public int run(PrintWriter out, PrintWriter err, String... args) {
throw new UnsupportedOperationException();
}
static final class RethrowableException extends Exception {
RethrowableException(Exception ex) {
super(Objects.requireNonNull(ex));
}
private static final long serialVersionUID = 1L;
}
@SuppressWarnings("unchecked")
private static <E extends Throwable> void throwAny(Throwable e) throws E {
throw (E)e;
}
private static final class LoopIterator<T> implements Iterator<T> {
LoopIterator(Iterable<T> iterable) {
this.iterable = Objects.requireNonNull(iterable);
rewind();
}
@Override
public boolean hasNext() {
return iter != null;
}
@Override
public T next() {
if (!hasNext()) {
throw new NoSuchElementException();
} else if (iter.hasNext()) {
return iter.next();
} else {
rewind();
if (!hasNext()) {
throw new NoSuchElementException();
} else {
return iter.next();
}
}
}
private void rewind() {
iter = Objects.requireNonNull(iterable.iterator());
if (!iter.hasNext()) {
iter = null;
}
}
private final Iterable<T> iterable;
private Iterator<T> iter;
}
static final class NonCompletable extends MockingToolProvider {
NonCompletable(String name, Iterable<CommandAction> actions) {
super(name, new LoopIterator<>(actions));
}
}
static final class Completable extends MockingToolProvider implements ToolProviderCompletableCommandMock {
Completable(String name, Iterable<CommandAction> actions) {
super(name, actions.iterator());
}
}
private final String name;
private final Iterator<CommandAction> actionIter;
static ToolProviderCommandMock UNREACHABLE = new MockingToolProvider.NonCompletable("<unreachable>", List.of());
}

View File

@ -0,0 +1,297 @@
/*
* Copyright (c) 2026, 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.mock;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
import jdk.jpackage.internal.util.IdentityWrapper;
/**
* Script of command mocks.
*/
public interface Script {
/**
* Returns a command mock for the given command line.
*
* @param cmdline the command line for which to look up a command mock
*
* @return a command mock matching the given command line
* @throws ScriptException if an internal script error occures
*/
CommandMock map(List<String> cmdline) throws ScriptException;
/**
* Returns command mocks registered with this object that have not completed yet.
*
* @See {@link CompletableCommandMock#completed()}
*
* @return the command mocks registered with this object that have not completed yet
*/
Collection<CompletableCommandMock> incompleteMocks();
public static Builder build() {
return new Builder();
}
public static <T> Predicate<List<String>> cmdlinePredicate(
Predicate<T> pred,
Function<String, T> conv,
Function<List<String>, Stream<String>> toStream) {
Objects.requireNonNull(pred);
Objects.requireNonNull(conv);
Objects.requireNonNull(toStream);
return cmdline -> {
return toStream.apply(cmdline).map(conv).filter(pred).findFirst().isPresent();
};
}
public static Predicate<List<String>> cmdlineContains(String arg) {
return cmdlinePredicate(Predicate.isEqual(Objects.requireNonNull(arg)), x -> x, List::stream);
}
public static Predicate<List<String>> cmdlineContains(Path arg) {
return cmdlinePredicate(Predicate.<Path>isEqual(Objects.requireNonNull(arg)), Path::of, List::stream);
}
public static Predicate<List<String>> cmdlineStartsWith(String arg) {
return cmdlinePredicate(Predicate.isEqual(Objects.requireNonNull(arg)), x -> x, cmdline -> {
return cmdline.stream().limit(1);
});
}
public static Predicate<List<String>> cmdlineStartsWith(Path arg) {
return cmdlinePredicate(Predicate.<Path>isEqual(Objects.requireNonNull(arg)), Path::of, cmdline -> {
return cmdline.stream().limit(1);
});
}
public final class ScriptException extends RuntimeException {
ScriptException(RuntimeException cause) {
super(Objects.requireNonNull(cause));
}
ScriptException(String msg) {
super(Objects.requireNonNull(msg));
}
private static final long serialVersionUID = 1L;
}
public final class Builder {
public Script createSequence() {
return new SequenceScript(List.copyOf(instructions), completableMocks());
}
public Script createLoop() {
return new LoopScript(List.copyOf(instructions), completableMocks());
}
public Builder map(Predicate<List<String>> pred, CommandMock mock) {
Objects.requireNonNull(pred);
Objects.requireNonNull(mock);
if (mock instanceof CompletableCommandMock completable) {
completableMocks.add(new IdentityWrapper<>(completable));
}
instruction(cmdline -> {
if (pred.test(cmdline)) {
return new CommandMockResult(Optional.of(mock));
} else {
return new CommandMockResult(Optional.empty());
}
});
return this;
}
public Builder map(Predicate<List<String>> pred, CommandMock.Builder mock) {
Optional.ofNullable(commandMockBuilderMutator).ifPresent(mock::mutate);
return map(pred, mock.create());
}
public Builder map(Predicate<List<String>> pred, CommandMockSpec mock) {
return map(pred, mock.toCommandMockBuilder());
}
public Builder map(CommandMockSpec mock) {
return map(cmdlineStartsWith(mock.name()), mock.toCommandMockBuilder());
}
public Builder use(CommandMock mock) {
return map(_ -> true, mock);
}
public Builder use(Predicate<List<String>> pred, CommandMock.Builder mock) {
return map(_ -> true, mock);
}
public Builder use(Predicate<List<String>> pred, CommandMockSpec mock) {
return map(_ -> true, mock);
}
public Builder branch(Predicate<List<String>> pred, Script script) {
Objects.requireNonNull(pred);
Objects.requireNonNull(script);
instruction(cmdline -> {
if (pred.test(cmdline)) {
return new ScriptResult(script);
} else {
return new CommandMockResult(Optional.empty());
}
});
return this;
}
public Builder commandMockBuilderMutator(Consumer<CommandMock.Builder> v) {
commandMockBuilderMutator = v;
return this;
}
public Builder mutate(Consumer<Script.Builder> mutator) {
mutator.accept(this);
return this;
}
private Builder instruction(Function<List<String>, Result> instruction) {
instructions.add(Objects.requireNonNull(instruction));
return this;
}
private Collection<CompletableCommandMock> completableMocks() {
return completableMocks.stream().map(IdentityWrapper::value).toList();
}
private static RuntimeException noMapping(List<String> cmdline) {
return new ScriptException(String.format("Mapping for %s command line not found", cmdline));
}
private sealed interface Result {
}
private record CommandMockResult(Optional<CommandMock> value) implements Result {
CommandMockResult {
Objects.requireNonNull(value);
}
}
private record ScriptResult(Script value) implements Result {
ScriptResult {
Objects.requireNonNull(value);
}
}
private abstract static class AbstractScript implements Script {
AbstractScript(Collection<CompletableCommandMock> completableMocks) {
this.completableMocks = Objects.requireNonNull(completableMocks);
}
@Override
public Collection<CompletableCommandMock> incompleteMocks() {
return completableMocks.stream().filter(Predicate.not(CompletableCommandMock::completed)).toList();
}
private final Collection<CompletableCommandMock> completableMocks;
}
private static final class LoopScript extends AbstractScript {
LoopScript(List<Function<List<String>, Result>> instructions,
Collection<CompletableCommandMock> completableMocks) {
super(completableMocks);
this.instructions = Objects.requireNonNull(instructions);
}
@Override
public CommandMock map(List<String> cmdline) {
for (var instruction : instructions) {
switch (instruction.apply(cmdline)) {
case CommandMockResult result -> {
var mock = result.value();
if (mock.isPresent()) {
return mock.get();
}
}
case ScriptResult result -> {
return result.value().map(cmdline);
}
}
}
throw noMapping(cmdline);
}
private final List<Function<List<String>, Result>> instructions;
}
private static final class SequenceScript extends AbstractScript {
SequenceScript(List<Function<List<String>, Result>> instructions,
Collection<CompletableCommandMock> completableMocks) {
super(completableMocks);
this.iter = instructions.iterator();
}
@Override
public CommandMock map(List<String> cmdline) {
if (!iter.hasNext()) {
throw new ScriptException("No more mappings");
} else {
switch (iter.next().apply(cmdline)) {
case CommandMockResult result -> {
var mock = result.value();
if (mock.isPresent()) {
return mock.get();
}
}
case ScriptResult result -> {
return result.value().map(cmdline);
}
}
}
throw noMapping(cmdline);
}
private final Iterator<Function<List<String>, Result>> iter;
}
private Consumer<CommandMock.Builder> commandMockBuilderMutator = CommandMock.Builder::noRepeats;
private final List<Function<List<String>, Result>> instructions = new ArrayList<>();
private final Set<IdentityWrapper<CompletableCommandMock>> completableMocks = new HashSet<>();
}
}

View File

@ -0,0 +1,179 @@
/*
* Copyright (c) 2026, 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.mock;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.IntStream;
/**
* Specification of a {@link Script}.
*/
public record ScriptSpec(List<Item> items, boolean loop) {
public ScriptSpec {
Objects.requireNonNull(items);
}
@Override
public String toString() {
var sb = new StringBuilder();
sb.append(items.toString());
if (loop) {
// Append "Clockwise Gapped Circle Arrow" Unicode symbol.
sb.append('(').appendCodePoint(0x27F3).append(')');
}
return sb.toString();
}
public Script create() {
var script = Script.build();
items.forEach(item -> {
item.applyTo(script, loop);
});
if (loop) {
return script.createLoop();
} else {
return script.createSequence();
}
}
public Collection<Path> commandNames() {
return items.stream().map(Item::mockSpec).map(CommandMockSpec::name).distinct().toList();
}
private record Item(CommandMockSpec mockSpec, int repeatCount, boolean detailedDescription) {
private Item {
Objects.requireNonNull(mockSpec);
if (repeatCount < 0) {
throw new IllegalArgumentException();
}
}
@Override
public String toString() {
var sb = new StringBuilder();
if (detailedDescription) {
sb.append(mockSpec);
} else if (mockSpec.isDefaultMockName()) {
sb.append(mockSpec.name());
} else {
sb.append(mockSpec.mockName());
}
if (repeatCount > 0) {
sb.append('(').append(repeatCount + 1).append(')');
}
return sb.toString();
}
void applyTo(Script.Builder script, boolean loopScript) {
var pred = Script.cmdlineStartsWith(mockSpec.name());
var mockBuilder = mockSpec.toCommandMockBuilder();
if (loopScript) {
script.map(pred, mockBuilder.repeat(repeatCount).create());
} else {
mockBuilder.repeat(0);
IntStream.rangeClosed(0, repeatCount).forEach(_ -> {
script.map(pred, mockBuilder.create());
});
}
}
}
public static Builder build() {
return new Builder();
}
public static final class Builder {
private Builder() {
}
public ScriptSpec create() {
return new ScriptSpec(List.copyOf(items), loop);
}
public Builder loop(boolean v) {
loop = v;
return this;
}
public Builder loop() {
return loop(true);
}
public final class ItemBuilder {
private ItemBuilder(CommandMockSpec mockSpec) {
this.mockSpec = Objects.requireNonNull(mockSpec);
}
public Builder add() {
items.add(new Item(mockSpec, repeat, detailedDescription));
return Builder.this;
}
public ItemBuilder repeat(int v) {
if (repeat < 0) {
throw new IllegalArgumentException();
}
repeat = v;
return this;
}
public ItemBuilder detailedDescription(boolean v) {
detailedDescription = v;
return this;
}
public ItemBuilder detailedDescription() {
return detailedDescription(true);
}
private final CommandMockSpec mockSpec;
private int repeat;
private boolean detailedDescription;
}
public Builder add(CommandMockSpec mockSpec) {
return build(mockSpec).add();
}
public Builder addLoop(CommandMockSpec mockSpec) {
return build(mockSpec).add();
}
public ItemBuilder build(CommandMockSpec mockSpec) {
return new ItemBuilder(mockSpec);
}
private final List<Item> items = new ArrayList<>();
private boolean loop;
}
}

View File

@ -0,0 +1,66 @@
/*
* Copyright (c) 2026, 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.mock;
import java.nio.file.Path;
import java.util.Objects;
/**
* Specification of a {@link Script} bound to a specific directory.
*/
public class ScriptSpecInDir {
public ScriptSpecInDir() {
}
@Override
public String toString() {
return scriptSpec.toString();
}
public boolean isPathInDir(Path path) {
return path.startsWith(dir);
}
public ScriptSpecInDir dir(Path v) {
dir = v;
return this;
}
public ScriptSpecInDir scriptSpec(ScriptSpec v) {
scriptSpec = v;
return this;
}
public ScriptSpec scriptSpec() {
Objects.requireNonNull(dir);
return Objects.requireNonNull(scriptSpec);
}
public Script create() {
return scriptSpec().create();
}
private ScriptSpec scriptSpec;
private Path dir;
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2026, 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.mock;
import java.util.spi.ToolProvider;
public sealed interface ToolProviderCommandMock extends CommandMock, ToolProvider
permits ToolProviderCompletableCommandMock, MockingToolProvider {
}

View File

@ -0,0 +1,27 @@
/*
* Copyright (c) 2026, 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.mock;
public sealed interface ToolProviderCompletableCommandMock extends ToolProviderCommandMock, CompletableCommandMock
permits MockingToolProvider.Completable {
}

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) 2026, 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.mock;
public enum VerbatimCommandMock implements CommandMock {
INSTANCE
}

View File

@ -0,0 +1,54 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal;
import static org.junit.jupiter.api.Assertions.assertEquals;
import jdk.jpackage.test.mock.CommandActionSpecs;
import jdk.jpackage.test.mock.CommandMockExit;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
public class LibProvidersLookupTest {
@ParameterizedTest
@EnumSource(value = CommandMockExit.class)
public void test_supported(CommandMockExit exit) {
var ldd = CommandActionSpecs.build().exit(exit).toCommandMockBuilder().name("ldd-mock").create();
Globals.main(() -> {
Globals.instance().executorFactory(() -> {
return new Executor().mapper(executor -> {
return executor.copy().mapper(null).toolProvider(ldd);
});
});
boolean actual = LibProvidersLookup.supported();
assertEquals(exit.exitNormally(), actual);
return 0;
});
}
}

View File

@ -0,0 +1,154 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal;
import static jdk.jpackage.internal.model.StandardPackageType.LINUX_DEB;
import static jdk.jpackage.internal.model.StandardPackageType.LINUX_RPM;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import jdk.jpackage.internal.model.StandardPackageType;
import jdk.jpackage.internal.util.Result;
import jdk.jpackage.test.mock.CommandActionSpecs;
import jdk.jpackage.test.mock.CommandMockExit;
import jdk.jpackage.test.mock.CommandMockSpec;
import jdk.jpackage.test.mock.Script;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
public class LinuxPackageArchTest {
@ParameterizedTest
@MethodSource
public void test(Runnable test) {
test.run();
}
private static List<Runnable> test() {
var data = new ArrayList<Runnable>();
// "foo" stdout interleaved with "bar" stderr
var fooArch = CommandActionSpecs.build()
.printToStdout("f").printToStderr("b")
.printToStdout("o").printToStderr("a")
.printToStdout("o").printToStderr("r");
for (var exit : CommandMockExit.values()) {
var dpkg = fooArch.copy().printToStdout("-deb").exit(exit).create();
data.add(new DebTestSpec(dpkg, Optional.of("foo-deb").filter(_ -> {
return exit.succeed();
})));
}
for (var rpmbuildExit : CommandMockExit.values()) {
var rpmbuild = fooArch.copy().printToStdout("-rpmbuild").exit(rpmbuildExit).create();
for (var rpmExit : CommandMockExit.values()) {
var rpm = fooArch.copy().printToStdout("-rpm").exit(rpmExit).create();
Optional<String> expect;
if (rpmbuildExit.succeed()) {
expect = Optional.of("foo-rpmbuild");
rpm = CommandActionSpecs.UNREACHABLE;
} else {
if (rpmExit.succeed()) {
expect = Optional.of("foo-rpm");
} else {
expect = Optional.empty();
}
}
data.add(new RpmTestSpec(rpmbuild, rpm, expect));
}
}
return data;
}
record RpmTestSpec(CommandActionSpecs rpmbuild, CommandActionSpecs rpm, Optional<String> expect) implements Runnable {
RpmTestSpec {
Objects.requireNonNull(rpm);
Objects.requireNonNull(rpmbuild);
Objects.requireNonNull(expect);
}
@Override
public void run() {
// Create an executor factory that will:
// - Substitute the "rpm" command with `rpm` mock.
// - Substitute the "rpmbuild" command with `rpmbuild` mock.
// - Throw if a command with the name other than "rpm" and "rpmbuild" is requested for execution.
var script = Script.build()
// LinuxPackageArch must run the "rpmbuild" command first. Put its mapping at the first position.
.map(new CommandMockSpec("rpmbuild", rpmbuild))
// LinuxPackageArch may optionally run the "rpm" command. Put its mapping after the "rpmbuild" command mapping.
.map(new CommandMockSpec("rpm", rpm))
// Create a sequential script: after every Script#map() call, the script will advance the current mapping.
// This means each mapping in the script will be considered only once.
// If "rpm" and "rpmbuild" commands are executed in reverse order, the second Script#map() will throw.
.createSequence();
test(expect, LINUX_RPM, script);
}
}
record DebTestSpec(CommandActionSpecs dpkg, Optional<String> expect) implements Runnable {
DebTestSpec {
Objects.requireNonNull(dpkg);
Objects.requireNonNull(expect);
}
@Override
public void run() {
var script = Script.build().map(new CommandMockSpec("dpkg", dpkg)).createSequence();
test(expect, LINUX_DEB, script);
}
}
private static void test(Optional<String> expectedArch, StandardPackageType pkgType, Script script) {
Globals.main(() -> {
MockUtils.buildJPackage().script(script).applyToGlobals();
Result<LinuxPackageArch> arch = LinuxPackageArch.create(pkgType);
assertEquals(arch.hasValue(), expectedArch.isPresent());
expectedArch.ifPresent(v -> {
assertEquals(v, arch.orElseThrow().value());
});
assertEquals(List.of(), script.incompleteMocks());
return 0;
});
}
}

View File

@ -0,0 +1,101 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import jdk.jpackage.internal.model.StandardPackageType;
import jdk.jpackage.test.mock.CommandActionSpecs;
import jdk.jpackage.test.mock.CommandMockExit;
import jdk.jpackage.test.mock.CommandMockSpec;
import jdk.jpackage.test.mock.Script;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
public class LinuxSystemEnvironmentTest {
@ParameterizedTest
@MethodSource
public void test_detectNativePackageType(DetectNativePackageTypeTestSpec test) {
test.run();
}
private static List<DetectNativePackageTypeTestSpec> test_detectNativePackageType() {
var data = new ArrayList<DetectNativePackageTypeTestSpec>();
for (var rpmExit : CommandMockExit.values()) {
for (var debExit : CommandMockExit.values()) {
CommandActionSpecs deb = CommandActionSpecs.build().exit(debExit).create();
CommandActionSpecs rpm;
Optional<StandardPackageType> expected;
if (debExit.succeed()) {
expected = Optional.of(StandardPackageType.LINUX_DEB);
rpm = CommandActionSpecs.UNREACHABLE;
} else {
rpm = CommandActionSpecs.build().exit(rpmExit).create();
if (rpmExit.succeed()) {
expected = Optional.of(StandardPackageType.LINUX_RPM);
} else {
expected = Optional.empty();
}
}
data.add(new DetectNativePackageTypeTestSpec(expected, rpm, deb));
}
}
return data;
}
record DetectNativePackageTypeTestSpec(Optional<StandardPackageType> expect, CommandActionSpecs rpm, CommandActionSpecs deb) {
DetectNativePackageTypeTestSpec {
Objects.requireNonNull(expect);
Objects.requireNonNull(rpm);
Objects.requireNonNull(deb);
}
void run() {
var script = Script.build()
.map(new CommandMockSpec("rpm", rpm))
.map(new CommandMockSpec("dpkg", deb))
.createLoop();
Globals.main(() -> {
MockUtils.buildJPackage().script(script).applyToGlobals();
var actual = LinuxSystemEnvironment.detectNativePackageType();
assertEquals(expect, actual);
assertEquals(List.of(), script.incompleteMocks());
return 0;
});
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -30,3 +30,35 @@
* ../../share/jdk.jpackage/jdk/jpackage/internal/model/ApplicationLayoutTest.java
* @run junit jdk.jpackage/jdk.jpackage.internal.LinuxApplicationLayoutTest
*/
/* @test
* @summary Test LinuxSystemEnvironment
* @requires (os.family == "linux")
* @library /test/jdk/tools/jpackage/helpers
* @build jdk.jpackage.test.mock.*
* @compile/module=jdk.jpackage -Xlint:all -Werror
* jdk/jpackage/internal/LinuxSystemEnvironmentTest.java
* ../../share/jdk.jpackage/jdk/jpackage/internal/MockUtils.java
* @run junit jdk.jpackage/jdk.jpackage.internal.LinuxSystemEnvironmentTest
*/
/* @test
* @summary Test LibProvidersLookup
* @requires (os.family == "linux")
* @library /test/jdk/tools/jpackage/helpers
* @build jdk.jpackage.test.mock.*
* @compile/module=jdk.jpackage -Xlint:all -Werror
* jdk/jpackage/internal/LibProvidersLookupTest.java
* @run junit jdk.jpackage/jdk.jpackage.internal.LibProvidersLookupTest
*/
/* @test
* @summary Test LinuxPackageArch
* @requires (os.family == "linux")
* @library /test/jdk/tools/jpackage/helpers
* @build jdk.jpackage.test.mock.*
* @compile/module=jdk.jpackage -Xlint:all -Werror
* jdk/jpackage/internal/LinuxPackageArchTest.java
* ../../share/jdk.jpackage/jdk/jpackage/internal/MockUtils.java
* @run junit jdk.jpackage/jdk.jpackage.internal.LinuxPackageArchTest
*/

View File

@ -0,0 +1,420 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal;
import static jdk.jpackage.internal.model.StandardPackageType.MAC_DMG;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jdk.internal.util.OperatingSystem;
import jdk.jpackage.internal.MacPackagingPipeline.MacBuildApplicationTaskID;
import jdk.jpackage.internal.PackagingPipeline.BuildApplicationTaskID;
import jdk.jpackage.internal.model.AppImageLayout;
import jdk.jpackage.internal.model.RuntimeBuilder;
import jdk.jpackage.internal.util.CommandOutputControl.UnexpectedResultException;
import jdk.jpackage.internal.util.FileUtils;
import jdk.jpackage.internal.util.RetryExecutor;
import jdk.jpackage.internal.util.function.ExceptionBox;
import jdk.jpackage.test.mock.CommandActionSpec;
import jdk.jpackage.test.mock.CommandActionSpecs;
import jdk.jpackage.test.mock.CommandMockSpec;
import jdk.jpackage.test.mock.MockIllegalStateException;
import jdk.jpackage.test.mock.ScriptSpec;
import jdk.jpackage.test.mock.ScriptSpecInDir;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
public class MacDmgPackagerTest {
/**
* Exercise branches in {@link MacDmgPackager#buildDMG()}.
*/
@ParameterizedTest
@MethodSource
public void test(DmgScript scriptSpec, @TempDir Path workDir) {
scriptSpec.run(workDir);
}
private static List<DmgScript> test() {
var data = new ArrayList<DmgScript>();
var succeed = CommandActionSpecs.build().exit().create();
var fail = CommandActionSpecs.build().exit(1).create();
// Test create
for (var createFullSucceed : List.of(true, false)) {
var dmgScript = new DmgScript();
var scriptBuilder = ScriptSpec.build();
if (createFullSucceed) {
// `hdiutil create -srcfolder` succeeds
scriptBuilder.add(new CommandMockSpec("hdiutil", "hdiutil-create", dmgScript.hdiutilCreate().exit().create()));
} else {
// `hdiutil create -srcfolder` fails
scriptBuilder.add(new CommandMockSpec("hdiutil", "hdiutil-create", fail));
scriptBuilder.add(new CommandMockSpec("hdiutil", "hdiutil-create-empty", dmgScript.hdiutilCreateEmpty().exit().create()));
}
scriptBuilder
// `hdiutil attach` succeeds
.add(new CommandMockSpec("hdiutil", "hdiutil-attach", succeed))
// `osascript` succeeds
.add(new CommandMockSpec("osascript", succeed))
// `hdiutil detach` succeeds
.add(new CommandMockSpec("hdiutil", "hdiutil-detach", dmgScript.hdiutilDetach().exit().create()))
// `hdiutil convert` succeeds
.add(new CommandMockSpec("hdiutil", "hdiutil-convert", dmgScript.hdiutilConvert().exit().create()));
data.add(dmgScript.scriptSpec(scriptBuilder.create()));
}
// Test detach
for (var detachResult : DetachResult.values()) {
var dmgScript = new DmgScript();
var scriptBuilder = ScriptSpec.build()
.add(new CommandMockSpec("hdiutil", "hdiutil-create", dmgScript.hdiutilCreate().exit().create()))
.add(new CommandMockSpec("hdiutil", "hdiutil-attach", succeed))
.add(new CommandMockSpec("osascript", succeed));
switch (detachResult) {
case ALL_FAIL -> {
dmgScript.expect(UnexpectedResultException.class);
scriptBuilder.build(new CommandMockSpec("hdiutil", "hdiutil-detach", fail)).repeat(9).add();
}
case LAST_SUCCEED -> {
scriptBuilder
.build(new CommandMockSpec("hdiutil", "hdiutil-detach", fail)).repeat(8).add()
.add(new CommandMockSpec("hdiutil", "hdiutil-detach", dmgScript.hdiutilDetach().exit().create()))
.add(new CommandMockSpec("hdiutil", "hdiutil-convert", dmgScript.hdiutilConvert().exit().create()));
}
case FIRST_SUCCEED_WITH_EXIT_1 -> {
scriptBuilder
.build(new CommandMockSpec("hdiutil", "hdiutil-detach", dmgScript.hdiutilDetach().exit(1).create()))
.detailedDescription().add()
.add(new CommandMockSpec("hdiutil", "hdiutil-convert", dmgScript.hdiutilConvert().exit().create()));
}
case FIRST_SUCCEED_MOUNT_POINT_REMAINS -> {
scriptBuilder
.build(new CommandMockSpec("hdiutil", "hdiutil-detach", dmgScript.hdiutilDetach(false).exit().create()))
.detailedDescription().add()
.add(new CommandMockSpec("hdiutil", "hdiutil-convert", dmgScript.hdiutilConvert().exit().create()));
}
}
data.add(dmgScript.scriptSpec(scriptBuilder.create()));
}
return data;
}
private enum DetachResult {
ALL_FAIL,
LAST_SUCCEED,
// The first `hdiutil detach` attempt exits with exit code "1" but deletes the mounted directory.
FIRST_SUCCEED_WITH_EXIT_1,
// The first `hdiutil detach` attempt exits with exit code "0" and the mounted directory stays undeleted.
FIRST_SUCCEED_MOUNT_POINT_REMAINS,
;
}
private static MacDmgSystemEnvironment createSysEnv(ScriptSpec scriptSpec) {
return new MacDmgSystemEnvironment(
Path.of("hdiutil"),
Path.of("osascript"),
Stream.of("SetFile").map(Path::of).filter(scriptSpec.commandNames()::contains).findFirst()
);
}
private static RuntimeBuilder createRuntimeBuilder() {
return new RuntimeBuilder() {
@Override
public void create(AppImageLayout appImageLayout) {
throw new UnsupportedOperationException();
}
};
}
private static void runPackagingMock(Path workDir, MacDmgSystemEnvironment sysEnv) {
var app = new ApplicationBuilder()
.appImageLayout(MacPackagingPipeline.APPLICATION_LAYOUT)
.runtimeBuilder(createRuntimeBuilder())
.name("foo")
.create();
var macApp = new MacApplicationBuilder(app).create();
var macDmgPkg = new MacDmgPackageBuilder(new MacPackageBuilder(new PackageBuilder(macApp, MAC_DMG))).create();
var buildEnv = new BuildEnvBuilder(workDir.resolve("build-root")).appImageDirFor(macDmgPkg).create();
var packager = new MacDmgPackager(buildEnv, macDmgPkg, workDir, sysEnv);
var pipelineBuilder = MacPackagingPipeline.build(Optional.of(packager.pkg()));
packager.accept(pipelineBuilder);
// Disable actions of tasks filling an application image.
Stream.concat(
Stream.of(BuildApplicationTaskID.values()),
Stream.of(MacBuildApplicationTaskID.values())
).forEach(taskId -> {
pipelineBuilder.task(taskId).noaction().add();
});
var contentMock = new ContentMock();
// Fill application image with content mock.
pipelineBuilder.task(BuildApplicationTaskID.CONTENT).applicationAction(env -> {
contentMock.create(env.resolvedLayout().contentDirectory());
}).add();
pipelineBuilder.create().execute(buildEnv, packager.pkg(), packager.outputDir());
var outputDmg = packager.outputDir().resolve(packager.pkg().packageFileNameWithSuffix());
contentMock.verifyStoredInFile(outputDmg);
}
private static final class DmgScript extends ScriptSpecInDir {
@Override
public String toString() {
var sb = new StringBuilder();
sb.append(super.toString());
Optional.ofNullable(expectedErrorType).ifPresent(type -> {
sb.append("; ").append(type.getCanonicalName());
});
return sb.toString();
}
@Override
public DmgScript scriptSpec(ScriptSpec v) {
super.scriptSpec(v);
return this;
}
DmgScript expect(Class<? extends Exception> v) {
expectedErrorType = v;
return this;
}
void run(Path workDir) {
var script = dir(Objects.requireNonNull(workDir)).create();
ExecutorFactory executorFactory = MockUtils.buildJPackage()
.script(script).listener(System.out::println).createExecutorFactory();
var objectFactory = ObjectFactory.build()
.executorFactory(executorFactory)
.retryExecutorFactory(new RetryExecutorFactory() {
@Override
public <T, E extends Exception> RetryExecutor<T, E> retryExecutor(Class<? extends E> exceptionType) {
return RetryExecutorFactory.DEFAULT.<T, E>retryExecutor(exceptionType).setSleepFunction(_ -> {
// Don't "sleep" to make the test run faster.
});
}
})
.create();
Globals.main(() -> {
Globals.instance().objectFactory(objectFactory);
if (expectedErrorType == null) {
runPackagingMock(workDir, createSysEnv(scriptSpec()));
} else {
var ex = assertThrows(Exception.class, () -> {
runPackagingMock(workDir, createSysEnv(scriptSpec()));
});
var cause = ExceptionBox.unbox(ex);
assertEquals(expectedErrorType, cause.getClass());
}
return 0;
});
assertEquals(List.of(), script.incompleteMocks());
}
CommandActionSpecs.Builder hdiutilCreate(boolean empty) {
CommandActionSpec action = CommandActionSpec.create("create", context -> {
var dstDmg = Path.of(context.optionValue("-ov"));
assertTrue(isPathInDir(dstDmg));
var volumeName = context.optionValue("-volname");
if (empty) {
createDmg(new CreateDmgResult(dstDmg, volumeName, Optional.empty()));
Files.createFile(dstDmg);
} else {
var srcDir = Path.of(context.optionValue("-srcfolder"));
assertTrue(isPathInDir(srcDir));
createDmg(new CreateDmgResult(dstDmg, volumeName, Optional.of(srcDir)));
try (var walk = Files.walk(srcDir)) {
var paths = walk.map(srcDir::relativize).map(Path::toString).toList();
Files.write(dstDmg, paths);
}
}
});
return CommandActionSpecs.build().action(action);
}
CommandActionSpecs.Builder hdiutilCreate() {
return hdiutilCreate(false);
}
CommandActionSpecs.Builder hdiutilCreateEmpty() {
return hdiutilCreate(true);
}
CommandActionSpecs.Builder hdiutilDetach() {
return hdiutilDetach(true);
}
CommandActionSpecs.Builder hdiutilDetach(boolean deleteMountPoint) {
var sb = new StringBuilder();
sb.append("detach");
if (!deleteMountPoint) {
sb.append("(rm-mount-point)");
}
CommandActionSpec action = CommandActionSpec.create(sb.toString(), context -> {
var mountPoint = Path.of(context.args().getLast());
assertTrue(isPathInDir(mountPoint));
try (var walk = Files.walk(mountPoint)) {
var dstDmg = dmg().dmg();
var paths = Stream.concat(
walk.map(mountPoint::relativize),
Files.readAllLines(dstDmg).stream().filter(Predicate.not(String::isEmpty)).map(Path::of)
).sorted().map(Path::toString).toList();
Files.write(dstDmg, paths);
}
if (deleteMountPoint) {
FileUtils.deleteRecursive(mountPoint);
}
});
return CommandActionSpecs.build().action(action);
}
CommandActionSpecs.Builder hdiutilConvert() {
CommandActionSpec action = CommandActionSpec.create("convert", context -> {
var srcDmg = Path.of(context.args().get(1));
assertTrue(isPathInDir(srcDmg));
var dstDmg = Path.of(context.args().getLast());
assertTrue(isPathInDir(dstDmg));
Files.copy(srcDmg, dstDmg);
});
return CommandActionSpecs.build().action(action);
}
private void createDmg(CreateDmgResult v) {
if (dmg != null) {
throw new MockIllegalStateException("The DMG already set");
} else {
dmg = Objects.requireNonNull(v);
}
}
private CreateDmgResult dmg() {
if (dmg == null) {
throw new MockIllegalStateException("The DMG not set");
} else {
return dmg;
}
}
private record CreateDmgResult(Path dmg, String volumeName, Optional<Path> srcFolder) {
CreateDmgResult {
Objects.requireNonNull(dmg);
Objects.requireNonNull(volumeName);
Objects.requireNonNull(srcFolder);
}
}
private CreateDmgResult dmg;
private Class<? extends Exception> expectedErrorType;
}
private static final class ContentMock {
void create(Path dir) throws IOException {
Files.createDirectories(dir.resolve("foo/bar"));
Files.writeString(dir.resolve("foo/bar/buz"), "Hello!");
if (!OperatingSystem.isWindows()) {
Files.createSymbolicLink(dir.resolve("symlink"), Path.of("foo"));
}
}
void verifyStoredInFile(Path file) {
try {
var expectedPaths = Stream.of(
Stream.of(Path.of("")),
DMG_ICON_FILES.stream(),
Stream.of(
Stream.of("foo/bar/buz"),
Stream.of("symlink").filter(_ -> {
return !OperatingSystem.isWindows();
})
).flatMap(x -> x).map(Path::of).map(Path.of("foo.app/Contents")::resolve)
).flatMap(x -> x).mapMulti(EXPAND_PATH).sorted().distinct().toList();
var actualPaths = Files.readAllLines(file).stream().map(Path::of).toList();
assertEquals(expectedPaths, actualPaths);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
}
private final static BiConsumer<Path, Consumer<Path>> EXPAND_PATH = (path, sink) -> {
do {
sink.accept(path);
path = path.getParent();
} while (path != null);
};
private final static List<Path> DMG_ICON_FILES = Stream.of(
".VolumeIcon.icns",
".background/background.tiff"
).map(Path::of).collect(Collectors.toUnmodifiableList());
}

View File

@ -0,0 +1,157 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import jdk.jpackage.internal.util.RetryExecutor;
import jdk.jpackage.test.mock.CommandActionSpecs;
import jdk.jpackage.test.mock.CommandMockExit;
import jdk.jpackage.test.mock.CommandMockSpec;
import jdk.jpackage.test.mock.Script;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
public class MacDmgSystemEnvironmentTest {
@ParameterizedTest
@MethodSource
void test_findSetFileUtility(FindSetFileUtilityTestSpec test) {
test.run();
}
private static List<FindSetFileUtilityTestSpec> test_findSetFileUtility() {
var data = new ArrayList<FindSetFileUtilityTestSpec>();
var succeed = CommandActionSpecs.build().exit().create();
for (var failureCause : List.of(CommandMockExit.EXIT_1, CommandMockExit.THROW_MOCK_IO_EXCEPTION)) {
var fail = CommandActionSpecs.build().exit(failureCause).create();
for (var i = 0; i != MacDmgSystemEnvironment.SETFILE_KNOWN_PATHS.size(); i++) {
var expected = MacDmgSystemEnvironment.SETFILE_KNOWN_PATHS.get(i);
var mocks = new ArrayList<CommandMockSpec>();
MacDmgSystemEnvironment.SETFILE_KNOWN_PATHS.subList(0, i).stream().map(failureSetFilePath -> {
return new CommandMockSpec(failureSetFilePath, fail);
}).forEach(mocks::add);
mocks.add(new CommandMockSpec(expected, succeed));
data.add(new FindSetFileUtilityTestSpec(Optional.of(expected), mocks));
}
var lastMocks = data.getLast().mockSpecs();
var lastSucceedMock = lastMocks.getLast();
var lastFailMock = new CommandMockSpec(lastSucceedMock.name(), lastSucceedMock.mockName(), fail);
var mocks = new ArrayList<>(lastMocks);
mocks.set(mocks.size() - 1, lastFailMock);
for (var xcrunOutout : List.<Map.Entry<Optional<String>, Boolean>>of(
// Use the path to the command of the current process
// as an output mock for the /usr/bin/xcrun command.
// MacDmgSystemEnvironment.findSetFileUtility() reads the command output
// and checks whether it is an executable file,
// so the hardcoded value is not an option for the output mock.
Map.entry(Optional.of(ProcessHandle.current().info().command().orElseThrow()), true),
// "/usr/bin/xcrun" outputs a path to non-executable file.
Map.entry(Optional.of("/dev/null"), false),
// "/usr/bin/xcrun" outputs '\0' making subsequent Path.of("\0") fail.
Map.entry(Optional.of("\0"), false),
// "/usr/bin/xcrun" doesn't output anything.
Map.entry(Optional.empty(), false)
)) {
mocks.add(new CommandMockSpec("/usr/bin/xcrun", CommandActionSpecs.build().mutate(builder -> {
xcrunOutout.getKey().ifPresent(builder::stdout);
}).exit(CommandMockExit.SUCCEED).create()));
Optional<String> expected;
if (xcrunOutout.getValue()) {
expected = xcrunOutout.getKey();
} else {
expected = Optional.empty();
}
data.add(new FindSetFileUtilityTestSpec(expected.map(Path::of), List.copyOf(mocks)));
mocks.removeLast();
}
// The last test case: "/usr/bin/xcrun" fails
mocks.add(new CommandMockSpec("/usr/bin/xcrun", fail));
data.add(new FindSetFileUtilityTestSpec(Optional.empty(), mocks));
}
return data;
}
record FindSetFileUtilityTestSpec(Optional<Path> expected, List<CommandMockSpec> mockSpecs) {
FindSetFileUtilityTestSpec {
Objects.requireNonNull(expected);
Objects.requireNonNull(mockSpecs);
}
@Override
public String toString() {
var tokens = new ArrayList<String>();
expected.ifPresent(v -> {
tokens.add(String.format("expect=%s", v));
});
tokens.add(mockSpecs.toString());
return tokens.stream().collect(Collectors.joining(", "));
}
void run() {
var script = Script.build().mutate(builder -> {
mockSpecs.forEach(builder::map);
}).createSequence();
Globals.main(() -> {
MockUtils.buildJPackage().script(script).applyToGlobals();
var actual = MacDmgSystemEnvironment.findSetFileUtility();
assertEquals(expected, actual);
assertEquals(List.of(), script.incompleteMocks());
return 0;
});
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -30,3 +30,25 @@
* ../../share/jdk.jpackage/jdk/jpackage/internal/model/ApplicationLayoutTest.java
* @run junit jdk.jpackage/jdk.jpackage.internal.MacApplicationLayoutTest
*/
/* @test
* @summary Test MacDmgSystemEnvironmentTest
* @requires (os.family == "mac")
* @library /test/jdk/tools/jpackage/helpers
* @build jdk.jpackage.test.mock.*
* @compile/module=jdk.jpackage -Xlint:all -Werror
* jdk/jpackage/internal/MacDmgSystemEnvironmentTest.java
* ../../share/jdk.jpackage/jdk/jpackage/internal/MockUtils.java
* @run junit jdk.jpackage/jdk.jpackage.internal.MacDmgSystemEnvironmentTest
*/
/* @test
* @summary Test MacDmgPackagerTest
* @requires (os.family == "mac")
* @library /test/jdk/tools/jpackage/helpers
* @build jdk.jpackage.test.mock.*
* @compile/module=jdk.jpackage -Xlint:all -Werror
* jdk/jpackage/internal/MacDmgPackagerTest.java
* ../../share/jdk.jpackage/jdk/jpackage/internal/MockUtils.java
* @run junit jdk.jpackage/jdk.jpackage.internal.MacDmgPackagerTest
*/

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2022, 2026, 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,15 +22,42 @@
*/
package jdk.jpackage.internal;
import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier;
import static jdk.jpackage.test.mock.CommandMock.ioerror;
import static jdk.jpackage.test.mock.CommandMock.succeed;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.spi.ToolProvider;
import java.util.stream.Stream;
import jdk.internal.util.OperatingSystem;
import jdk.jpackage.internal.cli.StandardBundlingOperation;
import jdk.jpackage.internal.model.AppImagePackageType;
import jdk.jpackage.internal.model.BundlingOperationDescriptor;
import jdk.jpackage.internal.model.PackageType;
import jdk.jpackage.internal.model.StandardPackageType;
import jdk.jpackage.test.Annotations;
import jdk.jpackage.test.HelloApp;
import jdk.jpackage.test.JUnitAdapter;
import jdk.jpackage.test.JavaAppDesc;
import jdk.jpackage.test.TKit;
import jdk.jpackage.test.mock.CommandActionSpecs;
import jdk.jpackage.test.mock.CommandMock;
import jdk.jpackage.test.mock.CommandMockExit;
import jdk.jpackage.test.mock.Script;
import org.junit.jupiter.api.Test;
public class DefaultBundlingEnvironmentTest {
public class DefaultBundlingEnvironmentTest extends JUnitAdapter {
@Test
void testDefaultBundlingOperation() {
@ -55,4 +82,200 @@ public class DefaultBundlingEnvironmentTest {
assertEquals(descriptor, env.defaultOperation().orElseThrow());
assertEquals(1, executed[0]);
}
/**
* Tests that commands executed to initialize the system environment are
* executed only once.
* @throws IOException
*/
@Annotations.Test
@Annotations.ParameterSupplier
public void testInitializedOnce(StandardBundlingOperation op) throws IOException {
List<List<String>> executedCommands = Collections.synchronizedList(new ArrayList<>());
var script = createMockScript(op);
ToolProvider jpackage = MockUtils.buildJPackage()
.os(op.os())
.script(script)
.listener(executedCommands::add).create();
var inputDir = TKit.createTempDirectory("input");
var appDesc = JavaAppDesc.parse(null);
HelloApp.createBundle(appDesc, inputDir);
//
// The command line should fail as the main class name is not specified and it is not set in the main jar.
//
// Run native packaging twice.
// It can execute commands required to configure the system environment in the first iteration.
// It must not execute a single command in the second iteration.
//
// Run app image packaging once.
// It must not execute a single command because app image packaging should not require native commands (Unless
// it is macOS where it will sign the app image with an ad hoc signature
// using the codesign tool. But: #1 - it is not a variable part of the system environment;
// #2 - jpackage should bail out earlier).
//
final var type = op.packageTypeValue();
final int iterationCount;
if (op.packageType() instanceof AppImagePackageType) {
iterationCount = 1;
} else {
iterationCount = 2;
}
for (var i = 0; i != iterationCount; i++) {
var result = new Executor().toolProvider(jpackage).saveOutput().args(
"--type=" + type,
"--input", inputDir.toString(),
"--main-jar", appDesc.jarFileName()).execute();
assertEquals(1, result.getExitCode());
// Assert it bailed out with the expected error.
assertEquals(List.of(
I18N.format("message.error-header", I18N.format("error.no-main-class-with-main-jar", appDesc.jarFileName())),
I18N.format("message.advice-header", I18N.format("error.no-main-class-with-main-jar.advice", appDesc.jarFileName()))
), result.stderr());
TKit.trace("The list of executed commands:");
executedCommands.forEach(cmdline -> {
TKit.trace(" " + cmdline);
});
TKit.trace("Done");
if (i == 0) {
executedCommands.clear();
}
}
assertEquals(List.of(), executedCommands);
assertEquals(List.of(), script.incompleteMocks());
}
public static List<Object[]> testInitializedOnce() {
return StandardBundlingOperation.ofPlatform(OperatingSystem.current())
.filter(StandardBundlingOperation::isCreateBundle).map(v -> {
return new Object[] {v};
}).toList();
}
private static Script createMockScript(StandardBundlingOperation op) {
if (op.packageType() instanceof AppImagePackageType) {
return Script.build().createSequence();
}
switch (op.os()) {
case WINDOWS -> {
return createWinMockScript();
}
case LINUX -> {
return createLinuxMockScript(op.packageType());
}
case MACOS -> {
return createMacMockScript();
}
default -> {
throw new AssertionError();
}
}
}
private static Script createWinMockScript() {
// Make "candle.exe" and "light.exe" always fail.
var candle = ioerror("candle-mock");
var light = ioerror("light-mock");
// Make the "wix.exe" functional.
var wix = CommandActionSpecs.build()
.stdout("5.0.2+aa65968c")
.exit(CommandMockExit.SUCCEED)
.toCommandMockBuilder().name("wix-mock").create();
var script = Script.build()
.map(Script.cmdlineStartsWith("candle.exe"), candle)
.map(Script.cmdlineStartsWith("light.exe"), light)
.map(Script.cmdlineStartsWith("wix.exe"), wix)
.createLoop();
return script;
}
private static Script createMacMockScript() {
@SuppressWarnings("unchecked")
var setfilePaths = (List<Path>)toSupplier(() -> {
return Class.forName(String.join(".",
DefaultBundlingEnvironmentTest.class.getPackageName(),
"MacDmgSystemEnvironment"
)).getDeclaredField("SETFILE_KNOWN_PATHS").get(null);
}).get();
var script = Script.build();
for (var setfilePath: setfilePaths) {
script.map(Script.cmdlineStartsWith(setfilePath), ioerror(setfilePath.toString() + "-mock"));
}
script.map(Script.cmdlineStartsWith("/usr/bin/xcrun"), succeed("/usr/bin/xcrun-mock"));
return script.createLoop();
}
private static Script createLinuxMockScript(PackageType pkgType) {
final Map<String, CommandMock> mocks = new HashMap<>();
var script = Script.build();
final Set<String> debCommandNames = Set.of("dpkg", "dpkg-deb", "fakeroot");
final Set<String> rpmCommandNames = Set.of("rpm", "rpmbuild");
final Set<String> succeedCommandNames;
switch (pkgType) {
case StandardPackageType.LINUX_DEB -> {
succeedCommandNames = debCommandNames;
// Simulate "dpkg --print-architecture".
var dpkg = CommandActionSpecs.build()
.stdout("foo-arch")
.exit(CommandMockExit.SUCCEED)
.toCommandMockBuilder().name("dpkg-mock").create();
mocks.put("dpkg", dpkg);
}
case StandardPackageType.LINUX_RPM -> {
succeedCommandNames = rpmCommandNames;
// Simulate "rpmbuild --version" prints the minimal acceptable version.
var rpmbuild = CommandActionSpecs.build()
.stdout("RPM version 4.10")
.exit(CommandMockExit.SUCCEED)
.toCommandMockBuilder().name("rpmbuild-mock").create();
mocks.put("rpmbuild", rpmbuild);
}
default -> {
throw new IllegalArgumentException();
}
}
script.map(Script.cmdlineStartsWith("ldd"), succeed("ldd-mock"));
for (var commandName : succeedCommandNames) {
if (!mocks.containsKey(commandName)) {
mocks.put(commandName, succeed(commandName + "-mock"));
}
}
Stream.of(debCommandNames, rpmCommandNames).flatMap(Set::stream).forEach(commandName -> {
var mock = Optional.ofNullable(mocks.get(commandName)).orElseGet(() -> {
return ioerror(commandName + "-mock");
});
script.map(Script.cmdlineStartsWith(commandName), mock);
});
return script.createLoop();
}
}

View File

@ -0,0 +1,165 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;
import java.util.stream.Stream;
import jdk.jpackage.internal.util.CommandOutputControl.UnexpectedResultException;
import jdk.jpackage.test.mock.CommandActionSpecs;
import jdk.jpackage.test.mock.CommandMockExit;
import jdk.jpackage.test.mock.CompletableCommandMock;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
public class ExecutorTest {
@ParameterizedTest
@MethodSource
public void test_retryOnKnownErrorMessage(RetryOnKnownErrorMessageTestSpec test) {
test.run();
}
private static Stream<RetryOnKnownErrorMessageTestSpec> test_retryOnKnownErrorMessage() {
var data = new ArrayList<RetryOnKnownErrorMessageTestSpec.Builder>();
final var subject = "French fries";
Supplier<RetryOnKnownErrorMessageTestSpec.Builder> build = () -> {
return RetryOnKnownErrorMessageTestSpec.build().subject(subject);
};
for (var exit : Stream.of(CommandMockExit.values()).filter(CommandMockExit::exitNormally).toList()) {
// These should succeed as there is no "French fries" in stderr.
Stream.of(
build.get().mock(CommandActionSpecs.build().stderr("Coleslaw").exit(exit)),
build.get().mock(CommandActionSpecs.build().stdout(subject).exit(exit)),
build.get()
// Fail in the first attempt (triggering text in the stderr)
.mock(CommandActionSpecs.build().stderr(subject).exit())
// Fail in the second attempt (same reason)
.repeatLastMoc()
// Pass in the next attempt (no triggering text in the stderr)
.mock(CommandActionSpecs.build().stderr("Coleslaw").exit(exit)),
build.get()
// Fail in the first attempt (triggering text in the stderr)
.mock(CommandActionSpecs.build().stderr(subject))
// Fail in the second attempt (error running the command)
.mock(CommandActionSpecs.build().exit(CommandMockExit.THROW_MOCK_IO_EXCEPTION))
// Pass in the next attempt (no triggering text in the stderr)
.mock(CommandActionSpecs.build().exit(exit))
).map(RetryOnKnownErrorMessageTestSpec.Builder::success).forEach(data::add);
}
// These should fail as there is "French fries" in stderr.
data.addAll(List.of(
// Try once and fail.
build.get().mock(CommandActionSpecs.build().stderr(subject).exit()),
// Try twice and fail.
build.get().mock(CommandActionSpecs.build().stderr(subject).exit()).repeatLastMoc()
));
return data.stream().map(RetryOnKnownErrorMessageTestSpec.Builder::create);
}
record RetryOnKnownErrorMessageTestSpec(List<CommandActionSpecs> mockSpecs, String subject, boolean success) {
RetryOnKnownErrorMessageTestSpec {
Objects.requireNonNull(mockSpecs);
Objects.requireNonNull(subject);
if (mockSpecs.isEmpty()) {
throw new IllegalArgumentException();
}
}
void run() {
var mock = mockSpecs.stream()
.reduce(CommandActionSpecs::andThen)
.orElseThrow().toCommandMockBuilder()
// Ensure attempts to run the command more times than expected will fail.
.noRepeats().create();
var retry = new Executor().toolProvider(mock).retryOnKnownErrorMessage(subject)
.setAttemptTimeout(null)
.setMaxAttemptsCount(mockSpecs.size());
if (success) {
assertDoesNotThrow(retry::execute);
} else {
assertThrowsExactly(UnexpectedResultException.class, retry::execute);
}
assertTrue(((CompletableCommandMock)mock).completed());
}
static Builder build() {
return new Builder();
}
static final class Builder {
RetryOnKnownErrorMessageTestSpec create() {
return new RetryOnKnownErrorMessageTestSpec(mockSpecs, subject, success);
}
public Builder mock(CommandActionSpecs v) {
mockSpecs.add(Objects.requireNonNull(v));
return this;
}
public Builder mock(CommandActionSpecs.Builder v) {
return mock(v.create());
}
public Builder repeatLastMoc() {
return mock(mockSpecs.getLast());
}
public Builder subject(String v) {
subject = v;
return this;
}
public Builder success(boolean v) {
success = v;
return this;
}
public Builder success() {
return success(true);
}
private final List<CommandActionSpecs> mockSpecs = new ArrayList<>();
private String subject;
private boolean success;
}
}
}

View File

@ -0,0 +1,235 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal;
import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier;
import java.io.PrintWriter;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.UnaryOperator;
import java.util.spi.ToolProvider;
import jdk.internal.util.OperatingSystem;
import jdk.jpackage.internal.cli.CliBundlingEnvironment;
import jdk.jpackage.internal.cli.Main;
import jdk.jpackage.internal.util.function.ExceptionBox;
import jdk.jpackage.test.mock.Script;
import jdk.jpackage.test.mock.ToolProviderCommandMock;
import jdk.jpackage.test.mock.VerbatimCommandMock;
/**
* Bridges "jdk.jpackage.internal" and "jdk.jpackage.test.mock" packages.
*/
public final class MockUtils {
private MockUtils() {
}
public static JPackageToolProviderBuilder buildJPackage() {
return new JPackageToolProviderBuilder();
}
public static final class JPackageToolProviderBuilder {
public ToolProvider create() {
return createJPackageToolProvider(os(), createObjectFactory());
}
public Consumer<Globals> createGlobalsMutator() {
var objectFactory = createObjectFactory();
return globals -> {
globals.objectFactory(objectFactory);
};
}
public void applyToGlobals() {
createGlobalsMutator().accept(Globals.instance());
}
ExecutorFactory createExecutorFactory() {
var commandMocksExecutorFactory = Optional.ofNullable(script).map(MockUtils::withCommandMocks).map(mapper -> {
return mapper.apply(ExecutorFactory.DEFAULT);
}).orElse(ExecutorFactory.DEFAULT);
var recordingExecutorFactory = Optional.ofNullable(listener).map(MockUtils::withCommandListener).map(mapper -> {
return mapper.apply(commandMocksExecutorFactory);
}).orElse(commandMocksExecutorFactory);
return recordingExecutorFactory;
}
ObjectFactory createObjectFactory() {
var executorFactory = createExecutorFactory();
if (executorFactory == ExecutorFactory.DEFAULT) {
return ObjectFactory.DEFAULT;
} else {
return ObjectFactory.build().executorFactory(executorFactory).create();
}
}
public JPackageToolProviderBuilder listener(Consumer<List<String>> v) {
listener = v;
return this;
}
public JPackageToolProviderBuilder script(Script v) {
script = v;
return this;
}
public JPackageToolProviderBuilder os(OperatingSystem v) {
os = v;
return this;
}
private OperatingSystem os() {
return Optional.ofNullable(os).orElseGet(OperatingSystem::current);
}
private Consumer<List<String>> listener;
private OperatingSystem os;
private Script script;
}
public static ToolProvider createJPackageToolProvider(OperatingSystem os, Script script) {
return buildJPackage()
.os(Objects.requireNonNull(os))
.script(Objects.requireNonNull(script))
.create();
}
public static ToolProvider createJPackageToolProvider(Script script) {
return createJPackageToolProvider(OperatingSystem.current(), script);
}
private static UnaryOperator<ExecutorFactory> withCommandListener(Consumer<List<String>> listener) {
Objects.requireNonNull(listener);
return executorFactory -> {
Objects.requireNonNull(executorFactory);
return () -> {
var executor = executorFactory.executor();
Optional<UnaryOperator<Executor>> oldMapper = executor.mapper();
UnaryOperator<Executor> newMapper = exec -> {
listener.accept(exec.commandLine());
return exec;
};
return executor.mapper(oldMapper.map(newMapper::compose).orElse(newMapper)::apply);
};
};
}
private static UnaryOperator<ExecutorFactory> withCommandMocks(Script script) {
return executorFactory -> {
Objects.requireNonNull(executorFactory);
return () -> {
var executor = executorFactory.executor();
Optional<UnaryOperator<Executor>> oldMapper = executor.mapper();
UnaryOperator<Executor> newMapper = exec -> {
var commandLine = exec.commandLine();
var mock = Objects.requireNonNull(script.map(commandLine));
switch (mock) {
case VerbatimCommandMock.INSTANCE -> {
// No mock for this command line.
return exec;
}
case ToolProviderCommandMock tp -> {
// Create a copy of the executor with the old mapper to prevent further recursion.
var copy = exec.copy().mapper(oldMapper.orElse(null));
copy.toolProvider(tp);
copy.args().clear();
copy.args(commandLine.subList(1, commandLine.size()));
return copy;
}
default -> {
// Unreachable because there are no other cases for this switch.
throw ExceptionBox.reachedUnreachable();
}
}
};
return executor.mapper(oldMapper.map(newMapper::compose).orElse(newMapper)::apply);
};
};
}
public static CliBundlingEnvironment createBundlingEnvironment(OperatingSystem os) {
Objects.requireNonNull(os);
String bundlingEnvironmentClassName;
switch (os) {
case WINDOWS -> {
bundlingEnvironmentClassName = "WinBundlingEnvironment";
}
case LINUX -> {
bundlingEnvironmentClassName = "LinuxBundlingEnvironment";
}
case MACOS -> {
bundlingEnvironmentClassName = "MacBundlingEnvironment";
}
default -> {
throw new IllegalArgumentException();
}
}
return toSupplier(() -> {
var ctor = Class.forName(String.join(".",
DefaultBundlingEnvironment.class.getPackageName(),
bundlingEnvironmentClassName
)).getConstructor();
return (CliBundlingEnvironment)ctor.newInstance();
}).get();
}
static ToolProvider createJPackageToolProvider(OperatingSystem os, ObjectFactory of) {
Objects.requireNonNull(os);
Objects.requireNonNull(of);
var impl = new Main.Provider(DefaultBundlingEnvironment.runOnce(() -> {
return createBundlingEnvironment(os);
}));
return new ToolProvider() {
@Override
public int run(PrintWriter out, PrintWriter err, String... args) {
return Globals.main(() -> {
Globals.instance().objectFactory(of);
return impl.run(out, err, args);
});
}
@Override
public String name() {
return impl.name();
}
};
}
}

View File

@ -4,7 +4,6 @@ ErrorTest.test(IMAGE; app-desc=Hello; args-add=[--app-version, 1.]; errors=[mess
ErrorTest.test(IMAGE; app-desc=Hello; args-add=[--app-version, 1.b.3]; errors=[message.error-header+[error.version-string-invalid-component, 1.b.3, b.3]])
ErrorTest.test(IMAGE; app-desc=Hello; args-add=[--app-version, ]; errors=[message.error-header+[error.version-string-empty]])
ErrorTest.test(IMAGE; app-desc=Hello; args-add=[--jlink-options, --add-modules]; errors=[message.error-header+[error.blocked.option, --add-modules]])
ErrorTest.test(IMAGE; app-desc=Hello; args-add=[--jlink-options, --foo]; errors=[message.error-header+[error.jlink.failed, Error: unknown option: --foo]])
ErrorTest.test(IMAGE; app-desc=Hello; args-add=[--jlink-options, --module-path]; errors=[message.error-header+[error.blocked.option, --module-path]])
ErrorTest.test(IMAGE; app-desc=Hello; args-add=[--jlink-options, --output]; errors=[message.error-header+[error.blocked.option, --output]])
ErrorTest.test(IMAGE; app-desc=Hello; args-add=[--main-jar, non-existent.jar]; errors=[message.error-header+[error.main-jar-does-not-exist, non-existent.jar]])

View File

@ -200,13 +200,13 @@ public class OptionsValidationFailTest {
Stream.of("--jpt-run=ErrorTest")
).flatMap(x -> x).toArray(String[]::new)).map(dynamicTest -> {
return DynamicTest.dynamicTest(dynamicTest.getDisplayName(), () -> {
JPackageCommand.withToolProvider(jpackageToolProviderMock, () -> {
JPackageCommand.withToolProvider(() -> {
try {
dynamicTest.getExecutable().execute();
} catch (Throwable t) {
throw ExceptionBox.toUnchecked(ExceptionBox.unbox(t));
}
});
}, jpackageToolProviderMock);
});
});
}

View File

@ -0,0 +1,168 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
public class CommandOutputControlTestUtils {
@ParameterizedTest
@MethodSource
public void test_isInterleave(TestSpec test) {
test.run();
}
private static Stream<TestSpec> test_isInterleave() {
var data = new ArrayList<TestSpec>();
data.addAll(List.of(
interleaved("Toaday", "Today", "a"),
interleaved("Todanaay", "Today", "ana"),
interleaved("aaaababaaa", "aaaba", "aabaa"),
interleaved("xxxxxxxxxxxyxxyx", "xxxxxxxy", "xxxxxyxx"),
interleaved("xyxxxxyxxxxxxxxx", "yxxxxxxx", "xxxyxxxx"),
interleaved("xxxxxxxyxxxxyxxx", "xxxyxxxx", "xxxxxxyx"),
interleaved("cbdddcdaadacdbddbdcdddccdabbadba", "cdddaaddbcdcdbab", "bdcadcbddddcabda"),
interleaved("ddbdcacddddbddbdbddadcaaccdcabab", "dbccdddbbddacdaa", "ddaddbdddacaccbb"),
interleaved("adccbacbacaacddadddcdbbddbbddddd", "acbcaacddddbdbdd", "dcabcadadcbdbddd"),
interleaved("abdbdabdaacdcdbddddadbbccddcddac", "addbaccbdddbcdda", "bbadaddddabcdcdc"),
interleaved("cdaacbddaabdddbddbddbddadbacccdc", "dabdadddbddabccc", "cacdabdbddbddacd"),
notInterleaved("Toady", "Today", "a"),
notInterleaved("", "Today", "a")
));
data.addAll(generateTestData("abcdefghijklmnopqrstuvwxyz", 10));
data.addAll(generateTestData("xxxxxxxy", 8));
data.addAll(generateTestData("aaabbbcccddddddd", 50));
return data.stream().flatMap(test -> {
return Stream.of(test, test.flip());
});
}
private static List<TestSpec> generateTestData(String src, int iteration) {
var srcCodePoints = new ArrayList<Integer>();
src.codePoints().mapToObj(Integer::valueOf).forEach(srcCodePoints::add);
var data = new ArrayList<TestSpec>();
Function<List<Integer>, String> toString = codePoints -> {
var arr = codePoints.stream().mapToInt(Integer::intValue).toArray();
return new String(arr, 0, arr.length);
};
for (int i = 0; i < 10; i++) {
Collections.shuffle(srcCodePoints);
var a = List.copyOf(srcCodePoints);
Collections.shuffle(srcCodePoints);
var b = List.copyOf(srcCodePoints);
var zip = new int[srcCodePoints.size() * 2];
for (int codePointIdx = 0; codePointIdx != a.size(); codePointIdx++) {
var dstIdx = codePointIdx * 2;
zip[dstIdx] = a.get(codePointIdx);
zip[dstIdx + 1] = b.get(codePointIdx);
}
data.add(interleaved(toString.apply(Arrays.stream(zip).boxed().toList()), toString.apply(a), toString.apply(b)));
}
return data;
}
public record TestSpec(String combined, String a, String b, boolean expected) {
public TestSpec {
Objects.requireNonNull(combined);
Objects.requireNonNull(a);
Objects.requireNonNull(b);
}
TestSpec flip() {
return new TestSpec(combined, b, a, expected);
}
void run() {
assertEquals(expected, isInterleave(
combined.chars().mapToObj(Integer::valueOf).toList(),
a.chars().mapToObj(Integer::valueOf).toList(),
b.chars().mapToObj(Integer::valueOf).toList()),
String.format("combined: %s; a=%s; b=%s", combined, a, b));
}
}
private static TestSpec interleaved(String combined, String a, String b) {
return new TestSpec(combined, a, b, true);
}
private static TestSpec notInterleaved(String combined, String a, String b) {
return new TestSpec(combined, a, b, false);
}
// Solves the standard "Find if a string C is an interleave of strings A and B."
// problem but use containers instead of strings.
static <T> boolean isInterleave(List<T> combined, List<T> a, List<T> b) {
if (a.size() + b.size() != combined.size()) {
return false;
}
final var n = a.size();
final var m = b.size();
var prev = new boolean[m + 1];
final var cur = new boolean[m + 1];
prev[0] = true;
for (int j = 1; j <= m; j++) {
prev[j] = prev[j - 1] && Objects.equals(b.get(j - 1), combined.get(j - 1));
}
for (int i = 1; i <= n; i++) {
cur[0] = prev[0] && Objects.equals(a.get(i - 1), combined.get(i - 1));
for (int j = 1; j <= m; j++) {
int k = i + j;
cur[j] = (prev[j] && Objects.equals(a.get(i - 1), combined.get(k - 1)))
|| (cur[j - 1] && Objects.equals(b.get(j - 1), combined.get(k - 1)));
}
prev = cur.clone();
}
return prev[m];
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2022, 2026, 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
@ -21,11 +21,13 @@
* questions.
*/
package jdk.jpackage.internal;
package jdk.jpackage.internal.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
@ -45,27 +47,41 @@ public class EnquoterTest {
assertEquals(expected, actual);
}
private static Stream<org.junit.jupiter.params.provider.Arguments> testForShellLiterals() {
@ParameterizedTest
@MethodSource
public void testIdentity(String input) {
var actual = Enquoter.identity().applyTo(input);
assertEquals(input, actual);
}
@ParameterizedTest
@MethodSource("testIdentity")
public void testNoEscaper(String input) {
var actual = Enquoter.identity().setEnquotePredicate(_ -> true).applyTo(input);
assertEquals('"' + input + '"', actual);
}
private static Stream<Arguments> testForShellLiterals() {
return Stream.of(
makeArguments("''", ""),
makeArguments("'foo'", "foo"),
makeArguments("' foo '", " foo "),
makeArguments("'foo bar'", "foo bar"),
makeArguments("'foo\\' bar'", "foo' bar")
Arguments.of("''", ""),
Arguments.of("'foo'", "foo"),
Arguments.of("' foo '", " foo "),
Arguments.of("'foo bar'", "foo bar"),
Arguments.of("'foo\\' bar'", "foo' bar")
);
}
private static Stream<org.junit.jupiter.params.provider.Arguments> testForPropertyValues() {
private static Stream<Arguments> testForPropertyValues() {
return Stream.of(
makeArguments("", ""),
makeArguments("foo", "foo"),
makeArguments("\" foo \"", " foo "),
makeArguments("\"foo bar\"", "foo bar"),
makeArguments("\"foo' bar\"", "foo' bar")
Arguments.of("", ""),
Arguments.of("foo", "foo"),
Arguments.of("\" foo \"", " foo "),
Arguments.of("\"foo bar\"", "foo bar"),
Arguments.of("\"foo' bar\"", "foo' bar")
);
}
static org.junit.jupiter.params.provider.Arguments makeArguments(Object ... args) {
return org.junit.jupiter.params.provider.Arguments.of(args);
private static Stream<String> testIdentity() {
return Stream.of("", "foo", " foo ", "foo bar", "foo' bar");
}
}

View File

@ -0,0 +1,331 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal.util;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.time.Duration;
import java.util.Objects;
import jdk.jpackage.internal.util.RetryExecutor.Context;
import jdk.jpackage.internal.util.function.ExceptionBox;
import jdk.jpackage.internal.util.function.ThrowingFunction;
import jdk.jpackage.internal.util.function.ThrowingSupplier;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
public class RetryExecutorTest {
@Test
public void test_defaults() {
var executor = new AttemptCounter<Void, Exception>(context -> {
throw new AttemptFailedException();
});
var defaultTimeout = Duration.ofSeconds(2);
var defaultAttemptCount = 5;
var timeout = Slot.<Duration>createEmpty();
assertThrowsExactly(AttemptFailedException.class, new RetryExecutor<Void, Exception>(Exception.class)
.setExecutable(executor)
.setSleepFunction(t -> {
assertEquals(defaultTimeout, t);
timeout.set(t);
return;
})::execute);
assertEquals(defaultTimeout, timeout.get());
assertEquals(defaultAttemptCount, executor.count());
}
@ParameterizedTest
@ValueSource(ints = {0, 1, 2, 3, -4})
public void test_N_attempts_fail(int maxAttemptsCount) throws AttemptFailedException {
var retry = new RetryExecutor<String, AttemptFailedException>(AttemptFailedException.class)
.setMaxAttemptsCount(maxAttemptsCount)
.setAttemptTimeout(null)
.setExecutable(context -> {
if (context.attempt() == (maxAttemptsCount - 1)) {
assertTrue(context.isLastAttempt());
} else {
assertFalse(context.isLastAttempt());
}
throw new AttemptFailedException("Attempt: " + context.attempt());
});
if (maxAttemptsCount <= 0) {
assertNull(retry.execute());
} else {
var ex = assertThrowsExactly(AttemptFailedException.class, retry::execute);
assertEquals("Attempt: " + (maxAttemptsCount - 1), ex.getMessage());
}
}
@ParameterizedTest
@ValueSource(ints = {1, 2, 3})
public void test_N_attempts_last_succeed(int maxAttemptsCount) throws AttemptFailedException {
test_N_attempts_M_succeed(maxAttemptsCount, maxAttemptsCount - 1, false);
}
@ParameterizedTest
@ValueSource(ints = {2, 3})
public void test_N_attempts_first_succeed(int maxAttemptsCount) throws AttemptFailedException {
test_N_attempts_M_succeed(maxAttemptsCount, 0, false);
}
@Test
public void test_N_attempts_2nd_succeed() throws AttemptFailedException {
test_N_attempts_M_succeed(4, 1, false);
}
@Test
public void test_N_attempts_2nd_succeed_unchecked() throws AttemptFailedException {
test_N_attempts_M_succeed(4, 1, true);
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void test_null_executor(boolean dynamic) {
var retry = new RetryExecutor<Void, AttemptFailedException>(AttemptFailedException.class)
.setAttemptTimeout(null).setMaxAttemptsCount(1000);
if (dynamic) {
int maxAttemptsCount = 3;
var executor = new AttemptCounter<Void, AttemptFailedException>(context -> {
assertTrue(context.attempt() <= (maxAttemptsCount - 1));
if (context.attempt() == (maxAttemptsCount - 1)) {
context.executor().setExecutable((ThrowingSupplier<Void, AttemptFailedException>)null);
}
throw new AttemptFailedException("foo");
});
retry.setExecutable(executor);
var ex = assertThrowsExactly(IllegalStateException.class, retry::execute);
assertEquals("No executable", ex.getMessage());
assertEquals(3, executor.count());
} else {
var ex = assertThrowsExactly(IllegalStateException.class, retry::execute);
assertEquals("No executable", ex.getMessage());
}
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void test_unexpected_exception(boolean executeUnchecked) {
var cause = new UnsupportedOperationException("foo");
var executor = new AttemptCounter<Void, IOException>(context -> {
assertEquals(0, context.attempt());
throw cause;
});
var retry = new RetryExecutor<Void, IOException>(IOException.class).setExecutable(executor)
.setMaxAttemptsCount(10).setAttemptTimeout(null);
UnsupportedOperationException ex;
if (executeUnchecked) {
ex = assertThrowsExactly(UnsupportedOperationException.class, retry::executeUnchecked);
} else {
ex = assertThrowsExactly(UnsupportedOperationException.class, retry::execute);
}
assertSame(cause, ex);
assertEquals(1, executor.count());
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void test_dynamic(boolean abort) {
int maxAttemptsCount = 4;
var secondExecutor = new AttemptCounter<String, AttemptFailedException>(context -> {
throw new AttemptFailedException("bar");
});
var firstExecutor = new AttemptCounter<String, AttemptFailedException>(context -> {
assertTrue(context.attempt() <= (maxAttemptsCount - 1));
if (context.attempt() == (maxAttemptsCount - 1)) {
if (abort) {
context.executor().setMaxAttemptsCount(maxAttemptsCount);
} else {
// Let it go two more times.
context.executor().setMaxAttemptsCount(maxAttemptsCount + 2);
}
context.executor().setExecutable(secondExecutor);
}
throw new AttemptFailedException("foo");
});
var retry = new RetryExecutor<String, AttemptFailedException>(AttemptFailedException.class)
.setExecutable(firstExecutor)
.setMaxAttemptsCount(1000000)
.setAttemptTimeout(null);
var ex = assertThrowsExactly(AttemptFailedException.class, retry::execute);
if (abort) {
assertEquals("foo", ex.getMessage());
assertEquals(0, secondExecutor.count());
} else {
assertEquals("bar", ex.getMessage());
assertEquals(2, secondExecutor.count());
}
assertEquals(maxAttemptsCount, firstExecutor.count());
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void test_supplier_executor(boolean isNull) throws Exception {
var retry = new RetryExecutor<String, Exception>(Exception.class).setMaxAttemptsCount(1);
if (isNull) {
retry.setExecutable((ThrowingSupplier<String, Exception>)null);
var ex = assertThrowsExactly(IllegalStateException.class, retry::execute);
assertEquals("No executable", ex.getMessage());
} else {
retry.setExecutable(() -> "Hello");
assertEquals("Hello", retry.execute());
}
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void test_executeUnchecked_fail(boolean withExceptionMapper) throws AttemptFailedException {
var retry = new RetryExecutor<String, AttemptFailedException>(AttemptFailedException.class).setExecutable(() -> {
throw new AttemptFailedException("kaput!");
}).setMaxAttemptsCount(1);
Class<? extends Exception> expectedExceptionType;
if (withExceptionMapper) {
retry.setExceptionMapper((AttemptFailedException ex) -> {
assertEquals("kaput!", ex.getMessage());
return new UncheckedAttemptFailedException(ex);
});
expectedExceptionType = UncheckedAttemptFailedException.class;
} else {
expectedExceptionType = ExceptionBox.class;
}
var ex = assertThrowsExactly(expectedExceptionType, retry::executeUnchecked);
assertEquals(AttemptFailedException.class, ex.getCause().getClass());
assertEquals("kaput!", ex.getCause().getMessage());
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void test_setSleepFunction(boolean withTimeout) {
var timeout = Slot.<Duration>createEmpty();
assertDoesNotThrow(new RetryExecutor<Void, AttemptFailedException>(AttemptFailedException.class)
.setMaxAttemptsCount(2)
.mutate(retry -> {
if (withTimeout) {
retry.setAttemptTimeout(Duration.ofDays(100));
} else {
retry.setAttemptTimeout(null);
}
})
.setExecutable(context -> {
if (context.isLastAttempt()) {
return null;
} else {
throw new AttemptFailedException();
}
})
.setSleepFunction(timeout::set)::execute);
assertEquals(withTimeout, timeout.find().isPresent());
if (withTimeout) {
assertEquals(Duration.ofDays(100), timeout.get());
}
}
private static void test_N_attempts_M_succeed(int maxAttempts, int failedAttempts, boolean unchecked) throws AttemptFailedException {
var countingExecutor = new AttemptCounter<String, AttemptFailedException>(context -> {
if (context.attempt() == failedAttempts) {
return "You made it!";
} else {
throw new AttemptFailedException();
}
});
var retry = new RetryExecutor<String, AttemptFailedException>(AttemptFailedException.class)
.setMaxAttemptsCount(maxAttempts)
.setAttemptTimeout(null)
.setExecutable(countingExecutor);
assertEquals("You made it!", unchecked ? retry.execute() : retry.executeUnchecked());
assertEquals(failedAttempts, countingExecutor.count() - 1);
}
private static final class AttemptCounter<T, E extends Exception> implements ThrowingFunction<Context<RetryExecutor<T, E>>, T, E> {
AttemptCounter(ThrowingFunction<Context<RetryExecutor<T, E>>, T, E> impl) {
this.impl = Objects.requireNonNull(impl);
}
@Override
public T apply(Context<RetryExecutor<T, E>> context) throws E {
counter++;
return impl.apply(context);
}
int count() {
return counter;
}
private int counter;
private final ThrowingFunction<Context<RetryExecutor<T, E>>, T, E> impl;
}
private static final class AttemptFailedException extends Exception {
AttemptFailedException(String msg) {
super(msg);
}
AttemptFailedException() {
}
private static final long serialVersionUID = 1L;
}
private static final class UncheckedAttemptFailedException extends RuntimeException {
UncheckedAttemptFailedException(AttemptFailedException ex) {
super(ex);
}
private static final long serialVersionUID = 1L;
}
}

View File

@ -441,10 +441,7 @@ public final class ErrorTest {
.error("error.no-module-in-path", "com.foo.bar"),
// non-existing argument file
testSpec().noAppDesc().notype().addArgs("@foo")
.error("ERR_CannotParseOptions", "foo"),
// invalid jlink option
testSpec().addArgs("--jlink-options", "--foo")
.error("error.jlink.failed", "Error: unknown option: --foo")
.error("ERR_CannotParseOptions", "foo")
).map(TestSpec.Builder::create).toList());
// --main-jar and --module-name

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -229,7 +229,7 @@ public class PostImageScriptTest {
cmd.saveConsoleOutput(true);
}).addBundleVerifier((cmd, result) -> {
final var imageDir = result.stdout().getOutput().stream().map(String::stripLeading).filter(str -> {
final var imageDir = result.stdout().stream().map(String::stripLeading).filter(str -> {
return str.startsWith(imageDirOutputPrefix);
}).map(str -> {
return str.substring(imageDirOutputPrefix.length());