mirror of
https://github.com/openjdk/jdk.git
synced 2026-01-28 12:09:14 +00:00
8374219: Fix issues in jpackage's Executor class
Reviewed-by: almatvee
This commit is contained in:
parent
f5fa9e40b0
commit
663a08331a
@ -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;
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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".
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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 {
|
||||
}
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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.
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 -> {};
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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+(.*)$");
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
@ -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<>();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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 {
|
||||
}
|
||||
@ -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 {
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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());
|
||||
}
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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]])
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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];
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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());
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user