8357404: jpackage should attempt to get a package version from the JDK's release file if the --version option is not specified [v9]

This commit is contained in:
alexander_matveev 2026-02-11 12:06:08 -08:00
parent 17707867d4
commit fc97db8a10
14 changed files with 374 additions and 213 deletions

View File

@ -27,6 +27,7 @@ package jdk.jpackage.internal;
import static jdk.jpackage.internal.FromOptions.buildApplicationBuilder;
import static jdk.jpackage.internal.FromOptions.createPackageBuilder;
import static jdk.jpackage.internal.LinuxPackagingPipeline.APPLICATION_LAYOUT;
import static jdk.jpackage.internal.cli.StandardOption.APP_VERSION;
import static jdk.jpackage.internal.cli.StandardOption.LINUX_APP_CATEGORY;
import static jdk.jpackage.internal.cli.StandardOption.LINUX_DEB_MAINTAINER_EMAIL;
import static jdk.jpackage.internal.cli.StandardOption.LINUX_MENU_GROUP;
@ -37,8 +38,9 @@ import static jdk.jpackage.internal.cli.StandardOption.LINUX_RPM_LICENSE_TYPE;
import static jdk.jpackage.internal.cli.StandardOption.LINUX_SHORTCUT_HINT;
import static jdk.jpackage.internal.model.StandardPackageType.LINUX_DEB;
import static jdk.jpackage.internal.model.StandardPackageType.LINUX_RPM;
import java.util.Optional;
import jdk.jpackage.internal.cli.Options;
import jdk.jpackage.internal.model.DottedVersion;
import jdk.jpackage.internal.model.Launcher;
import jdk.jpackage.internal.model.LinuxApplication;
import jdk.jpackage.internal.model.LinuxDebPackage;
@ -50,6 +52,10 @@ import jdk.jpackage.internal.model.StandardPackageType;
final class LinuxFromOptions {
static LinuxApplication createLinuxApplication(Options options) {
return createLinuxApplication(options, Optional.empty());
}
static LinuxApplication createLinuxApplication(Options options, Optional<StandardPackageType> type) {
final var launcherFromOptions = new LauncherFromOptions().faWithDefaultDescription();
@ -67,7 +73,20 @@ final class LinuxFromOptions {
appBuilder.launchers().map(LinuxPackagingPipeline::normalizeShortcuts).ifPresent(appBuilder::launchers);
return LinuxApplication.create(appBuilder.create());
var app = appBuilder.create();
if (!APP_VERSION.containsIn(options)) {
// User didn't explicitly specify the version on the command line. jpackage derived it from the input.
// In this case it should ensure the derived value is valid RPM version.
if (type.isPresent()) {
if (type.get().equals(LINUX_RPM)) {
app = ApplicationBuilder.normalizeVersion(app, app.version(),
LinuxFromOptions::normalizeRpmVersion);
}
}
}
return LinuxApplication.create(app);
}
static LinuxRpmPackage createLinuxRpmPackage(Options options, LinuxRpmSystemEnvironment sysEnv) {
@ -101,7 +120,7 @@ final class LinuxFromOptions {
private static LinuxPackageBuilder createLinuxPackageBuilder(Options options, LinuxSystemEnvironment sysEnv, StandardPackageType type) {
final var app = createLinuxApplication(options);
final var app = createLinuxApplication(options, Optional.of(type));
final var superPkgBuilder = createPackageBuilder(options, app, type);
@ -118,4 +137,15 @@ final class LinuxFromOptions {
return pkgBuilder;
}
static String normalizeRpmVersion(String version) {
// RPM does not support "-" symbol in version. In some case
// we might have "-" from "release" file version.
// Normalize version if it has "-" symbols. All other supproted version
// formats by "release" file should be supported by RPM.
if (version.contains("-")) {
return DottedVersion.lazy(version).toComponentsString();
}
return version;
}
}

View File

@ -231,10 +231,7 @@ final class MacFromOptions {
if (!APP_VERSION.containsIn(options)) {
// User didn't explicitly specify the version on the command line. jpackage derived it from the input.
// In this case it should ensure the derived value is valid MacOS version.
UnaryOperator<String> versionNormalizer = version -> {
return normalizeVersion(version);
};
app = ApplicationBuilder.normalizeVersion(app, app.version(), versionNormalizer);
app = ApplicationBuilder.normalizeVersion(app, app.version(), MacFromOptions::normalizeVersion);
}
final var appBuilder = new MacApplicationBuilder(app);
@ -358,11 +355,6 @@ final class MacFromOptions {
// When reading from release file it can be 1 or 3 or maybe more.
// We will always normalize to 3 components if needed.
DottedVersion ver = DottedVersion.lazy(version);
if (ver.getComponentsCount() > 3) {
return ver.trim(3).pad(3).toComponentsString();
} else {
// We should drop any characters. For example: "-ea".
return ver.toComponentsString();
}
return ver.trim(3).toComponentsString();
}
}

View File

@ -37,12 +37,11 @@ import java.util.stream.Stream;
public final class DottedVersion {
private DottedVersion(String version, boolean greedy) {
this.value = version;
if (version.isEmpty()) {
if (greedy) {
throw new IllegalArgumentException(I18N.getString("error.version-string-empty"));
} else {
this.components = new BigInteger[0];
this.components = new Component[0];
this.suffix = "";
}
} else {
@ -58,7 +57,7 @@ public final class DottedVersion {
}
try {
return new BigInteger(digits);
return new Component(digits);
} catch (NumberFormatException ex) {
if (!greedy) {
return null;
@ -68,7 +67,7 @@ public final class DottedVersion {
digits));
}
}
}).takeWhile(Objects::nonNull).toArray(BigInteger[]::new);
}).takeWhile(Objects::nonNull).toArray(Component[]::new);
suffix = ds.getUnprocessedString();
if (!suffix.isEmpty() && greedy) {
ds.throwException();
@ -76,6 +75,11 @@ public final class DottedVersion {
}
}
private DottedVersion(Component[] components, String suffix) {
this.components = components;
this.suffix = suffix;
}
private static class DigitsSupplier {
DigitsSupplier(String input) {
@ -211,9 +215,31 @@ public final class DottedVersion {
return Arrays.deepEquals(this.components, other.components);
}
public DottedVersion trim(int limit) {
if (limit < 0) {
throw new IllegalArgumentException();
} else if (limit >= components.length) {
return this;
} else {
return new DottedVersion(Arrays.copyOf(components, limit), suffix);
}
}
public DottedVersion pad(int limit) {
if (limit < 0) {
throw new IllegalArgumentException();
} else if (limit <= components.length) {
return this;
} else {
var newComponents = Arrays.copyOf(components, limit);
Arrays.fill(newComponents, components.length, newComponents.length, Component.ZERO);
return new DottedVersion(newComponents, suffix);
}
}
@Override
public String toString() {
return value;
return Stream.of(components).map(Component::toString).collect(Collectors.joining(".")) + suffix;
}
public String getUnprocessedSuffix() {
@ -221,35 +247,7 @@ public final class DottedVersion {
}
public String toComponentsString() {
return Stream.of(components).map(BigInteger::toString).collect(Collectors.joining("."));
}
public DottedVersion trim(int componentLimit) {
if (componentLimit < 0) {
throw new IllegalArgumentException();
}
if (components.length > componentLimit) {
components = Arrays.stream(components).limit(componentLimit)
.toArray(BigInteger[]::new);
}
return this;
}
public DottedVersion pad(int componentLimit) {
if (componentLimit <= 0) {
throw new IllegalArgumentException();
}
if (components.length < componentLimit) {
final int origLength = components.length;
components = Arrays.copyOf(components, componentLimit);
Arrays.fill(components, origLength, components.length,
new BigInteger("0"));
}
return this;
return Stream.of(components).map(Component::parsedValue).map(BigInteger::toString).collect(Collectors.joining("."));
}
public int getComponentsCount() {
@ -257,10 +255,27 @@ public final class DottedVersion {
}
public BigInteger[] getComponents() {
return components;
return Stream.of(components).map(Component::parsedValue).toArray(BigInteger[]::new);
}
private BigInteger[] components;
private final String value;
private record Component(BigInteger parsedValue, String strValue) {
Component {
Objects.requireNonNull(parsedValue);
Objects.requireNonNull(strValue);
}
Component(String strValue) {
this(new BigInteger(strValue), strValue);
}
@Override
public String toString() {
return strValue;
}
static final Component ZERO = new Component(BigInteger.ZERO, "0");
}
private final Component[] components;
private final String suffix;
}

View File

@ -86,10 +86,7 @@ final class WinFromOptions {
if (!APP_VERSION.containsIn(options)) {
// User didn't explicitly specify the version on the command line. jpackage derived it from the input.
// In this case it should ensure the derived value is valid Windows version.
UnaryOperator<String> versionNormalizer = version -> {
return normalizeVersion(version);
};
app = ApplicationBuilder.normalizeVersion(app, app.version(), versionNormalizer);
app = ApplicationBuilder.normalizeVersion(app, app.version(), WinFromOptions::normalizeVersion);
}
return WinApplication.create(app);
@ -137,7 +134,7 @@ final class WinFromOptions {
// When reading from release file it can be 1 or 3 or maybe more.
// We will always normalize to 4 components if needed.
DottedVersion ver = DottedVersion.lazy(version);
if (ver.getComponentsCount() != 2 || ver.getComponentsCount() != 4) {
if (ver.getComponentsCount() != 2 && ver.getComponentsCount() != 4) {
return ver.trim(4).pad(4).toComponentsString();
} else {
// We should drop any characters. For example: "-ea".

View File

@ -248,6 +248,11 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
return getArgumentValue("--input", () -> null, Path::of);
}
private boolean isLinuxRpmPackageType() {
return TKit.isLinux() && (PackageType.LINUX_RPM == getArgumentValue("--type",
() -> null, PACKAGE_TYPES::get));
}
public String version() {
return Optional.ofNullable(getArgumentValue("--app-version")).or(() -> {
if (isRuntime()) {
@ -259,6 +264,8 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
return WindowsHelper.getNormalizedVersion(releaseVersion.toString());
} else if (TKit.isOSX()) {
return MacHelper.getNormalizedVersion(releaseVersion.toString());
} else if (isLinuxRpmPackageType()) {
return LinuxHelper.getNormalizedRpmVersion(releaseVersion.toString());
} else {
return releaseVersion.toString();
}

View File

@ -49,6 +49,7 @@ import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import jdk.jpackage.internal.model.DottedVersion;
import jdk.jpackage.internal.util.PathUtils;
import jdk.jpackage.internal.util.function.ThrowingConsumer;
import jdk.jpackage.test.LauncherShortcut.InvokeShortcutSpec;
@ -127,6 +128,18 @@ public final class LinuxHelper {
getDefaultPackageArch(packageType)) + packageType.getSuffix();
}
static String getNormalizedRpmVersion(String version) {
// RPM does not support "-" symbol in version. In some case
// we might have "-" from "release" file version.
// Normalize version if it has "-" symbols. All other supproted version
// formats by "release" file should be supported by RPM.
if (version.contains("-")) {
return DottedVersion.lazy(version).toComponentsString();
}
return version;
}
public static Stream<Path> getPackageFiles(JPackageCommand cmd) {
cmd.verifyIsOfType(PackageType.LINUX);

View File

@ -1114,11 +1114,7 @@ public final class MacHelper {
// macOS requires 1, 2 or 3 components version string.
// We will always normalize to 3 components if needed.
DottedVersion ver = DottedVersion.lazy(version);
if (ver.getComponentsCount() > 3) {
return ver.trim(3).pad(3).toComponentsString();
} else {
return ver.toComponentsString();
}
return ver.trim(3).toComponentsString();
}
static Path getInstallationDirectory(JPackageCommand cmd) {

View File

@ -61,7 +61,7 @@ public class WindowsHelper {
// Windows requires 2 or 4 components version string.
// We will always normalize to 4 components if needed.
DottedVersion ver = DottedVersion.lazy(version);
if (ver.getComponentsCount() != 2 || ver.getComponentsCount() != 4) {
if (ver.getComponentsCount() != 2 && ver.getComponentsCount() != 4) {
return ver.trim(4).pad(4).toComponentsString();
} else {
return ver.toComponentsString();

View File

@ -32,8 +32,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
@ -41,34 +44,26 @@ public class DottedVersionTest {
public record TestConfig(String input,
Function<String, DottedVersion> createVersion, String expectedSuffix,
int expectedComponentCount, String expectedToComponent, int numberOfComponents) {
int expectedComponentCount, String expectedToComponent) {
TestConfig(String input, Type type, int expectedComponentCount, String expectedToComponent) {
this(input, type.createVersion, "", expectedComponentCount, expectedToComponent, -1);
this(input, type.createVersion, "", expectedComponentCount, expectedToComponent);
}
TestConfig(String input, Type type, int expectedComponentCount) {
this(input, type.createVersion, "", expectedComponentCount, input, -1);
}
TestConfig(String input, Type type, String expectedToComponent, int numberOfComponents) {
this(input, type.createVersion, "", -1, expectedToComponent, numberOfComponents);
this(input, type.createVersion, "", expectedComponentCount, input);
}
static TestConfig greedy(String input, int expectedComponentCount, String expectedToComponent) {
return new TestConfig(input, Type.GREEDY.createVersion, "", expectedComponentCount, expectedToComponent, -1);
return new TestConfig(input, Type.GREEDY.createVersion, "", expectedComponentCount, expectedToComponent);
}
static TestConfig greedy(String input, int expectedComponentCount) {
return new TestConfig(input, Type.GREEDY.createVersion, "", expectedComponentCount, input, -1);
return new TestConfig(input, Type.GREEDY.createVersion, "", expectedComponentCount, input);
}
static TestConfig lazy(String input, String expectedSuffix, int expectedComponentCount, String expectedToComponent) {
return new TestConfig(input, Type.LAZY.createVersion, expectedSuffix, expectedComponentCount, expectedToComponent, -1);
}
static TestConfig lazy(String input, String expectedToComponent, int numberOfComponents) {
return new TestConfig(input, Type.LAZY.createVersion, "", -1, expectedToComponent, numberOfComponents);
return new TestConfig(input, Type.LAZY.createVersion, expectedSuffix, expectedComponentCount, expectedToComponent);
}
}
@ -124,42 +119,99 @@ public class DottedVersionTest {
@ParameterizedTest
@MethodSource
public void testTrim(TestConfig cfg) {
var dv = cfg.createVersion.apply(cfg.input());
assertEquals(cfg.expectedToComponent(),
dv.trim(cfg.numberOfComponents()).toComponentsString());
}
private static List<TestConfig> testTrim() {
List<TestConfig> data = new ArrayList<>();
data.addAll(List.of(
TestConfig.lazy("", "", 0),
TestConfig.lazy("1", "", 0),
TestConfig.lazy("1.2.3", "1", 1),
TestConfig.lazy("1.2.3", "1.2", 2),
TestConfig.lazy("1.2.3", "1.2.3", 3),
TestConfig.lazy("1.2.3", "1.2.3", 4)
));
return data;
public void testTrim(DottedVersion ver, String expectedStr, int limit) {
var expected = DottedVersion.lazy(expectedStr);
var actual = ver.trim(limit);
assertEquals(expected, actual);
if (limit >= ver.getComponents().length) {
assertSame(ver, actual);
} else {
assertNotSame(ver, actual);
}
assertEquals(expectedStr, actual.toString());
}
@ParameterizedTest
@MethodSource
public void testPad(TestConfig cfg) {
var dv = cfg.createVersion.apply(cfg.input());
assertEquals(cfg.expectedToComponent(),
dv.pad(cfg.numberOfComponents()).toComponentsString());
public void testTrimNegative(DottedVersion ver, int limit) {
assertThrowsExactly(IllegalArgumentException.class, () -> {
ver.trim(limit);
});
}
private static List<TestConfig> testPad() {
List<TestConfig> data = new ArrayList<>();
data.addAll(List.of(
TestConfig.lazy("", "0", 1),
TestConfig.lazy("1", "1", 1),
TestConfig.lazy("1", "1.0", 2),
TestConfig.lazy("1.2.3", "1.2.3.0.0", 5)
));
return data;
private static Stream<Arguments> testTrim() {
var testCases = new ArrayList<Arguments>();
for (var suffix : List.of("", ".foo")) {
testCases.addAll(List.of(
Arguments.of("1.02.3" + suffix, "" + suffix, 0),
Arguments.of("1.02.3" + suffix, "1" + suffix, 1),
Arguments.of("1.02.3" + suffix, "1.02" + suffix, 2),
Arguments.of("1.02.3" + suffix, "1.02.3" + suffix, 3),
Arguments.of("1.02.3" + suffix, "1.02.3" + suffix, 4)
));
}
return testCases.stream().map(DottedVersionTest::mapFirstStringToDottedVersion);
}
private static Stream<Arguments> testTrimNegative() {
return Stream.of(
Arguments.of("10.5.foo", -1)
).map(DottedVersionTest::mapFirstStringToDottedVersion);
}
@ParameterizedTest
@MethodSource
public void testPad(DottedVersion ver, String expectedStr, int limit) {
var expected = DottedVersion.lazy(expectedStr);
var actual = ver.pad(limit);
assertEquals(expected, actual);
if (limit <= ver.getComponents().length) {
assertSame(ver, actual);
} else {
assertNotSame(ver, actual);
}
assertEquals(expectedStr, actual.toString());
}
@ParameterizedTest
@MethodSource
public void testPadNegative(DottedVersion ver, int limit) {
assertThrowsExactly(IllegalArgumentException.class, () -> {
ver.pad(limit);
});
}
private static Stream<Arguments> testPad() {
var testCases = new ArrayList<Arguments>();
for (var suffix : List.of("", ".foo")) {
testCases.addAll(List.of(
Arguments.of("" + suffix, "" + suffix, 0),
Arguments.of("1.02.3" + suffix, "1.02.3" + suffix, 0),
Arguments.of("" + suffix, "0" + suffix, 1),
Arguments.of("1" + suffix, "1" + suffix, 1),
Arguments.of("1" + suffix, "1.0" + suffix, 2),
Arguments.of("1.02.3" + suffix, "1.02.3.0.0" + suffix, 5)
));
}
return testCases.stream().map(DottedVersionTest::mapFirstStringToDottedVersion);
}
private static Stream<Arguments> testPadNegative() {
return Stream.of(
Arguments.of("10.5.foo", -1)
).map(DottedVersionTest::mapFirstStringToDottedVersion);
}
private static Arguments mapFirstStringToDottedVersion(Arguments v) {
var objs = v.get();
objs[0] = DottedVersion.lazy((String)objs[0]);
return Arguments.of(objs);
}
record InvalidVersionTestSpec(String version, String invalidComponent) {

View File

@ -44,41 +44,43 @@ public class RuntimeVersionReaderTest {
@ParameterizedTest
@CsvSource({
"27.1.2, true",
"27.1.2, false",
"27.1.2-ea, true",
"27.1.2-ea, false"
"\"27.1.2\"",
"27.1.2",
"\"27.1.2-ea\"",
"27.1.2-ea",
"\"27.1.2+15\"",
"27.1.2+15"
})
public void test_release_file_with_version(String version,
boolean quoteVersion, @TempDir Path workdir) {
public void test_release_file_with_version(String version, @TempDir Path workdir) {
final var value = RuntimeVersionReader.readVersion(
createPropFileWithValue(workdir, "JAVA_VERSION", version, quoteVersion));
createPropFileWithValue(workdir, "JAVA_VERSION", version));
assertTrue(value.isPresent());
value.ifPresent(val -> {
assertEquals(version, value.get().toString());
// Version return by readVersion is always unquoted
String expectedVersion = version.replaceAll("^\"|\"$", "");
assertEquals(expectedVersion, value.get().toString());
});
}
@ParameterizedTest
@CsvSource({
"7.1.2+foo, true",
"foo, true",
"'', true",
"7.1.2+foo, false",
"foo, false",
"'', false"
"\"7.1.2+foo\"",
"\"foo\"",
"\"\"",
"7.1.2+foo",
"foo",
"''"
})
public void test_release_file_with_invalid_version(String version,
boolean quoteVersion, @TempDir Path workdir) {
public void test_release_file_with_invalid_version(String version, @TempDir Path workdir) {
final var value = RuntimeVersionReader.readVersion(
createPropFileWithValue(workdir, "JAVA_VERSION", version, quoteVersion));
createPropFileWithValue(workdir, "JAVA_VERSION", version));
assertFalse(value.isPresent());
}
@Test
public void test_release_file_without_version(@TempDir Path workdir) {
final var value = RuntimeVersionReader.readVersion(
createPropFileWithValue(workdir, "JDK_VERSION", "27.1.2", true));
createPropFileWithValue(workdir, "JDK_VERSION", "\"27.1.2\""));
assertFalse(value.isPresent());
}
@ -88,15 +90,10 @@ public class RuntimeVersionReaderTest {
assertFalse(value.isPresent());
}
private Path createPropFileWithValue(Path workdir, String name, String value,
boolean quoteValue) {
private Path createPropFileWithValue(Path workdir, String name, String value) {
Path releaseFile = workdir.resolve("release");
Properties props = new Properties();
if (quoteValue) {
props.setProperty(name, "\"" + value + "\"");
} else {
props.setProperty(name, value);
}
props.setProperty(name, value);
try (Writer writer = Files.newBufferedWriter(releaseFile)) {
props.store(writer, null);

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -62,13 +62,25 @@ public final class AppVersionTest {
"3.3"}},
{"7.8", "com.other/com.other.Hello", new String[]{"--app-version",
"4", "--app-version", "7.8"}},
// Pick version from jar
{"3.10.17", "com.other/com.other.Hello@3.10.17", null},
// Ignore version in jar if --app-version given
{"7.5.81", "com.other/com.other.Hello@3.10.17", new String[]{
"--app-version", "7.5.81"}}
}));
if (TKit.isWindows()) {
// Windows will normalize unsupproted number of components
// for jar, module or release file versions.
data.addAll(List.of(new Object[][]{
// Pick version from jar
{"3.10.17.0", "com.other/com.other.Hello@3.10.17", null},
}));
} else {
data.addAll(List.of(new Object[][]{
// Pick version from jar
{"3.10.17", "com.other/com.other.Hello@3.10.17", null},
}));
}
return data;
}

View File

@ -28,7 +28,9 @@ import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import javax.xml.xpath.XPathExpressionException;
import jdk.jpackage.internal.util.MacBundle;
import jdk.jpackage.test.AppImageFile;
import jdk.jpackage.test.HelloApp;
import jdk.jpackage.test.JavaAppDesc;
@ -53,9 +55,8 @@ import jdk.jpackage.test.TKit;
public final class ModulePathTest3 {
public ModulePathTest3(String jlinkOutputSubdir, String runtimeSubdir) {
this.jlinkOutputSubdir = Path.of(jlinkOutputSubdir);
this.runtimeSubdir = Path.of(runtimeSubdir);
public ModulePathTest3(RuntimeType runtimeType) {
this.runtimeType = Objects.requireNonNull(runtimeType);
}
/**
@ -74,8 +75,24 @@ public final class ModulePathTest3 {
HelloApp.createBundle(appDesc, moduleOutputDir);
final Path workDir = TKit.createTempDirectory("runtime").resolve("data");
final Path jlinkOutputDir = workDir.resolve(jlinkOutputSubdir);
Files.createDirectories(jlinkOutputDir.getParent());
final Path jlinkOutputDir;
switch (runtimeType) {
case IMAGE -> {
jlinkOutputDir = workDir;
}
case MAC_BUNDLE -> {
var macBundle = new MacBundle(workDir);
// Create macOS bundle structure sufficient to pass jpackage validation.
Files.createDirectories(macBundle.homeDir().getParent());
Files.createDirectories(macBundle.macOsDir());
Files.createFile(macBundle.infoPlistFile());
jlinkOutputDir = macBundle.homeDir();
}
default -> {
throw new AssertionError();
}
}
new Executor()
.setToolProvider(JavaTool.JLINK)
@ -90,21 +107,13 @@ public final class ModulePathTest3 {
"--strip-native-commands")
.execute();
if (TKit.isOSX()) {
// MacBundle requires valid bundle
if (Files.exists(workDir.resolve("Contents/Home"))) {
Files.createFile(workDir.resolve("Contents/Info.plist"));
Files.createDirectories(workDir.resolve("Contents/MacOS"));
}
}
JPackageCommand cmd = new JPackageCommand()
.setDefaultAppName()
.setPackageType(PackageType.IMAGE)
.setDefaultInputOutput()
.removeArgumentWithValue("--input")
.addArguments("--module", appDesc.moduleName() + "/" + appDesc.className())
.setArgumentValue("--runtime-image", workDir.resolve(runtimeSubdir));
.setArgumentValue("--runtime-image", workDir);
cmd.executeAndAssertHelloAppImageCreated();
@ -116,23 +125,25 @@ public final class ModulePathTest3 {
}
@Parameters
public static Collection<?> data() {
final List<String[]> paths = new ArrayList<>();
paths.add(new String[] { "", "" });
public static Collection<Object[]> data() {
final List<RuntimeType> testCases = new ArrayList<>();
testCases.add(RuntimeType.IMAGE);
if (TKit.isOSX()) {
// On OSX jpackage should accept both runtime root and runtime home
// directories.
paths.add(new String[] { "Contents/Home", "" });
testCases.add(RuntimeType.MAC_BUNDLE);
}
List<Object[]> data = new ArrayList<>();
for (var pathCfg : paths) {
data.add(new Object[] { pathCfg[0], pathCfg[1] });
}
return data;
return testCases.stream().map(v -> {
return new Object[] {v};
}).toList();
}
private final Path jlinkOutputSubdir;
private final Path runtimeSubdir;
public enum RuntimeType {
IMAGE,
MAC_BUNDLE,
;
}
private final RuntimeType runtimeType;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved.
* 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
@ -24,18 +24,20 @@
import java.io.IOException;
import java.nio.file.Files;
import java.util.Collection;
import java.util.ArrayList;
import java.util.List;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import jdk.jpackage.internal.util.MacBundle;
import jdk.jpackage.test.Annotations.Parameters;
import jdk.jpackage.test.Annotations.Test;
import jdk.jpackage.test.Executor;
import jdk.jpackage.test.HelloApp;
import jdk.jpackage.test.JPackageCommand;
import jdk.jpackage.test.JavaAppDesc;
import jdk.jpackage.test.JavaTool;
import jdk.jpackage.test.TKit;
import jdk.jpackage.test.HelloApp;
/*
@ -50,9 +52,8 @@ import jdk.jpackage.test.HelloApp;
public final class NoMPathRuntimeTest {
public NoMPathRuntimeTest(String jlinkOutputSubdir, String runtimeSubdir) {
this.jlinkOutputSubdir = Path.of(jlinkOutputSubdir);
this.runtimeSubdir = Path.of(runtimeSubdir);
public NoMPathRuntimeTest(RuntimeType runtimeType) {
this.runtimeType = Objects.requireNonNull(runtimeType);
}
@Test
@ -65,8 +66,24 @@ public final class NoMPathRuntimeTest {
cmd.executePrerequisiteActions();
final Path workDir = TKit.createTempDirectory("runtime").resolve("data");
final Path jlinkOutputDir = workDir.resolve(jlinkOutputSubdir);
Files.createDirectories(jlinkOutputDir.getParent());
final Path jlinkOutputDir;
switch (runtimeType) {
case IMAGE -> {
jlinkOutputDir = workDir;
}
case MAC_BUNDLE -> {
var macBundle = new MacBundle(workDir);
// Create macOS bundle structure sufficient to pass jpackage validation.
Files.createDirectories(macBundle.homeDir().getParent());
Files.createDirectories(macBundle.macOsDir());
Files.createFile(macBundle.infoPlistFile());
jlinkOutputDir = macBundle.homeDir();
}
default -> {
throw new AssertionError();
}
}
// List of modules required for test app.
final var modules = new String[] {
@ -74,7 +91,7 @@ public final class NoMPathRuntimeTest {
"java.desktop"
};
Executor jlink = new Executor()
new Executor()
.setToolProvider(JavaTool.JLINK)
.dumpOutput()
.addArguments(
@ -82,26 +99,17 @@ public final class NoMPathRuntimeTest {
"--output", jlinkOutputDir.toString(),
"--strip-debug",
"--no-header-files",
"--no-man-pages");
jlink.addArguments("--add-modules", appDesc.moduleName(),
"--module-path", Path.of(cmd.getArgumentValue("--module-path"))
.resolve("hello.jar").toString());
jlink.execute();
if (TKit.isOSX()) {
// MacBundle requires valid bundle
if (Files.exists(workDir.resolve("Contents/Home"))) {
Files.createFile(workDir.resolve("Contents/Info.plist"));
Files.createDirectories(workDir.resolve("Contents/MacOS"));
}
}
"--no-man-pages"
)
.addArguments(
"--add-modules", appDesc.moduleName(),
"--module-path", Path.of(cmd.getArgumentValue("--module-path")).resolve("hello.jar").toString()
).execute();
// non-modular jar in current dir caused error whe no module-path given
cmd.removeArgumentWithValue("--module-path");
cmd.setArgumentValue("--runtime-image", workDir.resolve(runtimeSubdir));
cmd.setArgumentValue("--runtime-image", workDir);
Path junkJar = null;
try {
// create a non-modular jar in the current directory
@ -118,24 +126,26 @@ public final class NoMPathRuntimeTest {
}
@Parameters
public static Collection<?> data() {
public static Collection<Object[]> data() {
final List<String[]> paths = new ArrayList<>();
paths.add(new String[] { "", "" });
final List<RuntimeType> testCases = new ArrayList<>();
testCases.add(RuntimeType.IMAGE);
if (TKit.isOSX()) {
// On OSX jpackage should accept both runtime root and runtime home
// directories.
paths.add(new String[] { "Contents/Home", "" });
testCases.add(RuntimeType.MAC_BUNDLE);
}
List<Object[]> data = new ArrayList<>();
for (var pathCfg : paths) {
data.add(new Object[] { pathCfg[0], pathCfg[1] });
}
return data;
return testCases.stream().map(v -> {
return new Object[] {v};
}).toList();
}
private final Path jlinkOutputSubdir;
private final Path runtimeSubdir;
public enum RuntimeType {
IMAGE,
MAC_BUNDLE,
;
}
private final RuntimeType runtimeType;
}

View File

@ -115,6 +115,26 @@ public class RuntimePackageTest {
.run(Action.CREATE_AND_UNPACK);
}
@Test
@Parameter(value = {"foo"})
@Parameter(value = {""})
@Parameter(value = {"17.21.3+foo"})
public static void testInvalidReleaseFileVersion(String version) {
testReleaseFileVersion(version, false, Optional.empty());
}
@Test
@Parameter(value = {"17.21.3-ea", "LINUX_DEB"}, ifOS = LINUX)
public static void testValidReleaseFileVersion(String version, PackageType... packageTypes) {
testReleaseFileVersion(version, true, Optional.empty(), packageTypes);
}
@Test
@Parameter(value = {"17.21.3-ea", "17.21.3", "LINUX_RPM"}, ifOS = LINUX)
public static void testValidReleaseFileVersion(String version, String normalizedVersion, PackageType... packageTypes) {
testReleaseFileVersion(version, true, Optional.of(normalizedVersion), packageTypes);
}
@Test
// 27
@Parameter(value = {"27"}, ifOS = {LINUX, MACOS})
@ -126,10 +146,8 @@ public class RuntimePackageTest {
@Parameter(value = {"27.1.2.3"}, ifOS = {LINUX, WINDOWS})
// 27.1.2.3.4
@Parameter(value = {"27.1.2.3.4"}, ifOS = LINUX)
// 17.21.3-ea
@Parameter(value = {"17.21.3-ea"}, ifOS = LINUX)
public static void testReleaseFileVersion(String version) {
testReleaseFileVersion(version, version);
public static void testValidReleaseFileVersion(String version) {
testReleaseFileVersion(version, true, Optional.empty());
}
@Test
@ -145,9 +163,18 @@ public class RuntimePackageTest {
// 17.21.3-ea
@Parameter(value = {"17.21.3-ea", "17.21.3"}, ifOS = MACOS)
@Parameter(value = {"17.21.3-ea", "17.21.3.0"}, ifOS = WINDOWS)
public static void testReleaseFileVersion(String version, String normalizedVersion) {
new PackageTest()
.addInitializer(cmd -> {
public static void testValidReleaseFileVersion(String version, String normalizedVersion) {
testReleaseFileVersion(version, true, Optional.of(normalizedVersion));
}
private static void testReleaseFileVersion(String version,
boolean validReleaseFileVersion, Optional<String> normalizedVersion,
PackageType... packageTypes) {
var test = new PackageTest();
if (packageTypes.length != 0) {
test.forTypes(packageTypes);
}
test.addInitializer(cmd -> {
// Remove --input parameter from jpackage command line as we don't
// create input directory in the test and jpackage fails
// if --input references non existant directory.
@ -167,15 +194,17 @@ public class RuntimePackageTest {
props.store(writer, null);
}
// Validate output
cmd.validateOutput(JPackageStringBundle.MAIN
.cannedFormattedString("message.release-version",
version, runtimeImage.toString()));
// Normalization message is only printed if we did normalization.
if (!version.equals(normalizedVersion)) {
// Validate output only if release version is valid for release file
if (validReleaseFileVersion) {
cmd.validateOutput(JPackageStringBundle.MAIN
.cannedFormattedString("message.version-normalized",
normalizedVersion, version));
.cannedFormattedString("message.release-version",
version, runtimeImage.toString()));
// Normalization message is only printed if we did normalization.
normalizedVersion.ifPresent(nv -> {
cmd.validateOutput(JPackageStringBundle.MAIN
.cannedFormattedString("message.version-normalized",
nv, version));
});
}
})
// Just create package. It is enough to verify version in bundle name.