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

Reviewed-by: almatvee
This commit is contained in:
Alexey Semenyuk 2026-03-11 16:39:57 +00:00
parent 775f07e671
commit c315d1cd2b
16 changed files with 1560 additions and 139 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.StandardBundlingOperation.CREATE_LINUX_RPM;
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;
@ -39,6 +40,7 @@ import static jdk.jpackage.internal.model.StandardPackageType.LINUX_DEB;
import static jdk.jpackage.internal.model.StandardPackageType.LINUX_RPM;
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;
@ -67,6 +69,10 @@ final class LinuxFromOptions {
appBuilder.launchers().map(LinuxPackagingPipeline::normalizeShortcuts).ifPresent(appBuilder::launchers);
if (OptionUtils.bundlingOperation(options) == CREATE_LINUX_RPM) {
appBuilder.derivedVersionNormalizer(LinuxFromOptions::normalizeRpmVersion);
}
return LinuxApplication.create(appBuilder.create());
}
@ -118,4 +124,15 @@ final class LinuxFromOptions {
return pkgBuilder;
}
private 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 supported version
// formats by "release" file should be supported by RPM.
if (version.contains("-")) {
return DottedVersion.lazy(version).toComponentsString();
}
return version;
}
}

View File

@ -31,7 +31,6 @@ import static jdk.jpackage.internal.MacRuntimeValidator.validateRuntimeHasJliLib
import static jdk.jpackage.internal.MacRuntimeValidator.validateRuntimeHasNoBinDir;
import static jdk.jpackage.internal.OptionUtils.isBundlingOperation;
import static jdk.jpackage.internal.cli.StandardBundlingOperation.CREATE_MAC_PKG;
import static jdk.jpackage.internal.cli.StandardBundlingOperation.SIGN_MAC_APP_IMAGE;
import static jdk.jpackage.internal.cli.StandardOption.APPCLASS;
import static jdk.jpackage.internal.cli.StandardOption.ICON;
import static jdk.jpackage.internal.cli.StandardOption.MAC_APP_CATEGORY;
@ -65,6 +64,7 @@ import jdk.jpackage.internal.cli.OptionValue;
import jdk.jpackage.internal.cli.Options;
import jdk.jpackage.internal.cli.StandardFaOption;
import jdk.jpackage.internal.model.ApplicationLaunchers;
import jdk.jpackage.internal.model.DottedVersion;
import jdk.jpackage.internal.model.ExternalApplication;
import jdk.jpackage.internal.model.FileAssociation;
import jdk.jpackage.internal.model.Launcher;
@ -234,7 +234,7 @@ final class MacFromOptions {
superAppBuilder.launchers(new ApplicationLaunchers(MacLauncher.create(mainLauncher), launchers.additionalLaunchers()));
}
final var app = superAppBuilder.create();
superAppBuilder.derivedVersionNormalizer(MacFromOptions::normalizeVersion);
return superAppBuilder;
}
@ -389,4 +389,11 @@ final class MacFromOptions {
private static boolean hasPkgInstallerSignIdentity(Options options) {
return options.contains(MAC_SIGNING_KEY_NAME) || options.contains(MAC_INSTALLER_SIGN_IDENTITY);
}
private static String normalizeVersion(String version) {
// macOS requires 1, 2 or 3 components version string.
// When reading from release file it can be 1 or 3 or maybe more.
// We will always normalize to 3 components if needed.
return DottedVersion.lazy(version).trim(3).toComponentsString();
}
}

View File

@ -36,17 +36,20 @@ import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import jdk.jpackage.internal.model.AppImageLayout;
import jdk.jpackage.internal.model.Application;
import jdk.jpackage.internal.model.ApplicationLaunchers;
import jdk.jpackage.internal.model.ExternalApplication;
import jdk.jpackage.internal.model.Launcher;
import jdk.jpackage.internal.model.LauncherIcon;
import jdk.jpackage.internal.model.LauncherModularStartupInfo;
import jdk.jpackage.internal.model.LauncherStartupInfo;
import jdk.jpackage.internal.model.ResourceDirLauncherIcon;
import jdk.jpackage.internal.model.RuntimeBuilder;
import jdk.jpackage.internal.model.RuntimeLayout;
import jdk.jpackage.internal.util.RootedPath;
import jdk.jpackage.internal.util.RuntimeReleaseFile;
final class ApplicationBuilder {
@ -65,6 +68,8 @@ final class ApplicationBuilder {
appImageLayout = other.appImageLayout;
runtimeBuilder = other.runtimeBuilder;
launchers = other.launchers;
runtimeReleaseFile = other.runtimeReleaseFile;
derivedVersionNormalizer = other.derivedVersionNormalizer;
}
ApplicationBuilder copy() {
@ -86,7 +91,7 @@ final class ApplicationBuilder {
return new Application.Stub(
effectiveName,
Optional.ofNullable(description).orElse(effectiveName),
Optional.ofNullable(version).orElseGet(DEFAULTS::version),
validatedVersion(),
Optional.ofNullable(vendor).orElseGet(DEFAULTS::vendor),
Optional.ofNullable(copyright).orElseGet(DEFAULTS::copyright),
Optional.ofNullable(appDirSources).orElseGet(List::of),
@ -147,6 +152,11 @@ final class ApplicationBuilder {
Optional<String> version() {
return Optional.ofNullable(version);
}
ApplicationBuilder runtimeReleaseFile(Path v) {
runtimeReleaseFile = v;
return this;
}
ApplicationBuilder vendor(String v) {
@ -169,6 +179,54 @@ final class ApplicationBuilder {
return this;
}
ApplicationBuilder derivedVersionNormalizer(UnaryOperator<String> v) {
derivedVersionNormalizer = v;
return this;
}
private String validatedVersion() {
return Optional.ofNullable(version).or(() -> {
// Application version has not been specified explicitly. Derive it.
var derivedVersion = derivedVersion();
if (derivedVersionNormalizer != null) {
derivedVersion = derivedVersion.map(v -> {
var mappedVersion = derivedVersionNormalizer.apply(v);
if (!mappedVersion.equals(v)) {
Log.verbose(I18N.format("message.version-normalized", mappedVersion, v));
}
return mappedVersion;
});
}
return derivedVersion;
}).orElseGet(DEFAULTS::version);
}
private Optional<String> derivedVersion() {
if (appImageLayout instanceof RuntimeLayout && runtimeReleaseFile != null) {
try {
var releaseVersion = new RuntimeReleaseFile(runtimeReleaseFile).getJavaVersion().toString();
Log.verbose(I18N.format("message.release-version", releaseVersion));
return Optional.of(releaseVersion);
} catch (Exception ex) {
Log.verbose(ex);
return Optional.empty();
}
} else if (launchers != null) {
return launchers.mainLauncher().startupInfo()
.filter(LauncherModularStartupInfo.class::isInstance)
.map(LauncherModularStartupInfo.class::cast)
.flatMap(modularStartupInfo -> {
var moduleVersion = modularStartupInfo.moduleVersion();
moduleVersion.ifPresent(v -> {
Log.verbose(I18N.format("message.module-version", v, modularStartupInfo.moduleName()));
});
return moduleVersion;
});
} else {
return Optional.empty();
}
}
static <T extends Launcher> ApplicationLaunchers normalizeIcons(
ApplicationLaunchers appLaunchers, Optional<Path> resourceDir, BiFunction<T, Launcher, T> launcherOverrideCtor) {
@ -333,6 +391,8 @@ final class ApplicationBuilder {
private AppImageLayout appImageLayout;
private RuntimeBuilder runtimeBuilder;
private ApplicationLaunchers launchers;
private Path runtimeReleaseFile;
private UnaryOperator<String> derivedVersionNormalizer;
private static final Defaults DEFAULTS = new Defaults(
"1.0",

View File

@ -59,10 +59,10 @@ import jdk.jpackage.internal.model.Application;
import jdk.jpackage.internal.model.ApplicationLaunchers;
import jdk.jpackage.internal.model.ApplicationLayout;
import jdk.jpackage.internal.model.Launcher;
import jdk.jpackage.internal.model.LauncherModularStartupInfo;
import jdk.jpackage.internal.model.PackageType;
import jdk.jpackage.internal.model.RuntimeLayout;
import jdk.jpackage.internal.util.RootedPath;
import jdk.jpackage.internal.util.RuntimeReleaseFile;
final class FromOptions {
@ -212,19 +212,6 @@ final class FromOptions {
runtimeBuilderBuilder.modulePath(ensureBaseModuleInModulePath(MODULE_PATH.findIn(options).orElseGet(List::of)));
if (!APP_VERSION.containsIn(options)) {
// Version is not specified explicitly. Try to get it from the app's module.
launchers.mainLauncher().startupInfo().ifPresent(startupInfo -> {
if (startupInfo instanceof LauncherModularStartupInfo modularStartupInfo) {
modularStartupInfo.moduleVersion().ifPresent(moduleVersion -> {
appBuilder.version(moduleVersion);
Log.verbose(I18N.format("message.module-version",
moduleVersion, modularStartupInfo.moduleName()));
});
}
});
}
predefinedRuntimeDirectory.ifPresentOrElse(runtimeBuilderBuilder::forRuntime, () -> {
final var startupInfos = launchers.asList().stream()
.map(Launcher::startupInfo)
@ -239,6 +226,8 @@ final class FromOptions {
}
}
predefinedRuntimeDirectory.map(RuntimeReleaseFile::releaseFilePathInRuntime).ifPresent(appBuilder::runtimeReleaseFile);
return appBuilder;
}

View File

@ -24,17 +24,13 @@
*/
package jdk.jpackage.internal;
import java.io.IOException;
import java.io.Reader;
import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleReference;
import java.net.URI;
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.Properties;
import jdk.jpackage.internal.util.RuntimeReleaseFile;
record ModuleInfo(String name, Optional<String> version, Optional<String> mainClass, Optional<URI> location) {
@ -56,30 +52,16 @@ record ModuleInfo(String name, Optional<String> version, Optional<String> mainCl
// We can't extract info about version and main class of a module
// linked in external runtime without running ModuleFinder in that
// runtime. But this is too much work as the runtime might have been
// coocked without native launchers. So just make sure the module
// is linked in the runtime by simply analysing the data
// cooked without native launchers. So just make sure the module
// is linked in the runtime by simply analyzing the data
// of `release` file.
final Path releaseFile = cookedRuntime.resolve("release");
try (Reader reader = Files.newBufferedReader(releaseFile)) {
Properties props = new Properties();
props.load(reader);
String moduleList = props.getProperty("MODULES");
if (moduleList == null) {
try {
var cookedRuntimeModules = RuntimeReleaseFile.loadFromRuntime(cookedRuntime).getModules();
if (!cookedRuntimeModules.contains(moduleName)) {
return Optional.empty();
}
if ((moduleList.startsWith("\"") && moduleList.endsWith("\""))
|| (moduleList.startsWith("\'") && moduleList.endsWith(
"\'"))) {
moduleList = moduleList.substring(1, moduleList.length() - 1);
}
if (!List.of(moduleList.split("\\s+")).contains(moduleName)) {
return Optional.empty();
}
} catch (IOException|IllegalArgumentException ex) {
} catch (Exception ex) {
Log.verbose(ex);
return Optional.empty();
}

View File

@ -52,6 +52,8 @@ message.app-image-created=Succeeded in building output application image directo
message.debug-working-directory=Kept working directory for debug: {0}
message.module-version=Using version "{0}" from module "{1}" as application version
message.release-version=Using version "{0}" from "release" file of the predefined runtime as a package version
message.version-normalized=Using version "{0}" normalized to platform supported format from "{1}"
message.error-header=Error: {0}
message.advice-header=Advice to fix: {0}

View File

@ -0,0 +1,130 @@
/*
* 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.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.Properties;
public final class RuntimeReleaseFile {
public RuntimeReleaseFile(Path releaseFilePath) throws IOException {
// The implementation is based on the behavior of
// jdk.tools.jlink.internal.plugins.ReleaseInfoPlugin, which
// uses java.util.Properties to read/write the "release" file.
try (Reader reader = Files.newBufferedReader(releaseFilePath)) {
props = new Properties();
props.load(reader);
}
}
/**
* Returns path to the "runtime" file in the specified runtime directory.
*
* @param runtimeDir the path to a directory with the standard Java runtime
* structure
*/
public static Path releaseFilePathInRuntime(Path runtimeDir) {
return runtimeDir.resolve("release");
}
/**
* Creates the instance form the "runtime" file in the specified runtime
* directory.
* <p>
* Uses {@link #releaseFilePathInRuntime(Path)} to get the path to the "runtime"
* file in the specified runtime directory.
*
* @param runtimeDir the path to a directory with the standard Java runtime
* structure
*/
public static RuntimeReleaseFile loadFromRuntime(Path runtimeDir) throws IOException {
return new RuntimeReleaseFile(releaseFilePathInRuntime(runtimeDir));
}
/**
* Returns verbatim value of the property with the specified name or an empty
* {@code Optional} if there is no property with the specified name.
* <p>
* Property values in the "release" file are enclosed in double quotes.
* This method returns the value with the double quotes.
*
* @param propertyName the property name
*/
public Optional<String> findRawProperty(String propertyName) {
return Optional.ofNullable(props.getProperty(propertyName));
}
/**
* Returns unquoted value of the property with the specified name or an empty
* {@code Optional} if there is no property with the specified name.
* <p>
* Property values in the "release" file are enclosed in double quotes. This
* method returns the value without the double quotes.
*
* @param propertyName the property name
*/
public Optional<String> findProperty(String propertyName) {
return findRawProperty(propertyName).map(v -> {
if (v.charAt(0) == '"' && v.charAt(v.length() - 1) == '"') {
return v.substring(1, v.length() - 1);
} else {
return v;
}
});
}
/**
* Returns the value of the "JAVA_VERSION" property.
* <p>
* Will throw {@code NoSuchElementException} if there is no such property. Will
* use {@link Runtime.Version#parse(String)} method to parse version string. Any
* exception that it may yield will be passed to the caller verbatim.
*
* @throws NoSuchElementException if there is no such property
*/
public Runtime.Version getJavaVersion() {
return findProperty("JAVA_VERSION").map(Runtime.Version::parse).orElseThrow(NoSuchElementException::new);
}
/**
* Returns the value of the "MODULES" property.
*
* @throws NoSuchElementException if there is no such property
*/
public List<String> getModules() {
return findProperty("MODULES").map(v -> {
return List.of(v.split("\\s+"));
}).orElseThrow(NoSuchElementException::new);
}
private final Properties props;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -42,6 +42,7 @@ import static jdk.jpackage.internal.cli.StandardOption.WIN_UPGRADE_UUID;
import static jdk.jpackage.internal.model.StandardPackageType.WIN_MSI;
import jdk.jpackage.internal.cli.Options;
import jdk.jpackage.internal.model.DottedVersion;
import jdk.jpackage.internal.model.Launcher;
import jdk.jpackage.internal.model.WinApplication;
import jdk.jpackage.internal.model.WinExePackage;
@ -73,6 +74,8 @@ final class WinFromOptions {
appBuilder.launchers().map(WinPackagingPipeline::normalizeShortcuts).ifPresent(appBuilder::launchers);
appBuilder.derivedVersionNormalizer(WinFromOptions::normalizeVersion);
return WinApplication.create(appBuilder.create());
}
@ -112,4 +115,11 @@ final class WinFromOptions {
return pkgBuilder.create();
}
private static String normalizeVersion(String version) {
// Windows requires between 2 and 4 components version string.
// When reading from release file it can be 1 or 3 or maybe more.
// One component will be normalized to 2 and more then 4 will be trim to 4.
return DottedVersion.lazy(version).trim(4).pad(2).toComponentsString();
}
}

View File

@ -8,7 +8,7 @@ requires.properties = \
jpackage.test.SQETest \
jpackage.test.MacSignTests
maxOutputSize = 2000000
maxOutputSize = 10000000
# Run jpackage tests on windows platform sequentially.
# Having "share" directory in the list affects tests on other platforms.

View File

@ -26,6 +26,7 @@ import static java.util.stream.Collectors.toMap;
import static jdk.jpackage.internal.util.function.ThrowingConsumer.toConsumer;
import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction;
import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier;
import static jdk.jpackage.test.JPackageCommand.DEFAULT_VERSION;
import java.io.IOException;
import java.nio.file.Path;
@ -65,7 +66,7 @@ public record AppImageFile(String mainLauncherName, Optional<String> mainLaunche
}
public AppImageFile(String mainLauncherName, Optional<String> mainLauncherClassName) {
this(mainLauncherName, mainLauncherClassName, "1.0", false, Map.of(mainLauncherName, Map.of()));
this(mainLauncherName, mainLauncherClassName, DEFAULT_VERSION, false, Map.of(mainLauncherName, Map.of()));
}
public AppImageFile(String mainLauncherName, String mainLauncherClassName) {

View File

@ -43,6 +43,7 @@ import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
@ -60,7 +61,10 @@ import java.util.spi.ToolProvider;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import jdk.jpackage.internal.model.DottedVersion;
import jdk.jpackage.internal.util.MacBundle;
import jdk.jpackage.internal.util.Result;
import jdk.jpackage.internal.util.RuntimeReleaseFile;
import jdk.jpackage.internal.util.function.ExceptionBox;
import jdk.jpackage.internal.util.function.ThrowingConsumer;
import jdk.jpackage.internal.util.function.ThrowingFunction;
@ -102,6 +106,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
executeInDirectory = cmd.executeInDirectory;
winMsiLogFile = cmd.winMsiLogFile;
unpackedPackageDirectory = cmd.unpackedPackageDirectory;
explicitVersion = cmd.explicitVersion;
}
JPackageCommand createImmutableCopy() {
@ -246,22 +251,100 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
public String version() {
return PropertyFinder.findAppProperty(this,
PropertyFinder.cmdlineOptionWithValue("--app-version").or(cmd -> {
if (cmd.isRuntime() && PackageType.MAC.contains(cmd.packageType())) {
// This is a macOS runtime bundle.
var predefinedRuntimeBundle = MacBundle.fromPath(Path.of(cmd.getArgumentValue("--runtime-image")));
if (predefinedRuntimeBundle.isPresent()) {
// This is a macOS runtime bundle created from the predefined runtime bundle (not a predefined runtime directory).
// The version of this bundle should be copied from the Info.plist file of the predefined runtime bundle.
return MacHelper.readPList(predefinedRuntimeBundle.get().infoPlistFile()).findValue("CFBundleVersion");
}
}
return Optional.empty();
}),
PropertyFinder.<JPackageCommand>of(Optional.ofNullable(explicitVersion))
.or(PropertyFinder.cmdlineOptionWithValue("--app-version"))
.or(JPackageCommand::derivedVersion),
PropertyFinder.appImageFile(appImageFile -> {
return appImageFile.version();
})
).orElse("1.0");
).orElse(DEFAULT_VERSION);
}
private Optional<String> derivedVersion() {
if (isRuntime()) {
var predefinedRuntimePath = Path.of(getArgumentValue("--runtime-image"));
if (TKit.isOSX()) {
// This is a macOS runtime bundle.
return MacBundle.fromPath(predefinedRuntimePath).map(predefinedRuntimeBundle -> {
return Result.<Optional<String>>of(() -> {
// This is a macOS runtime bundle created from the predefined runtime bundle (not a predefined runtime directory).
// The version of this bundle should be copied from the Info.plist file of the predefined runtime bundle.
return MacHelper.readPList(predefinedRuntimeBundle.infoPlistFile()).findValue("CFBundleVersion");
}).value().flatMap(x -> x).or(() -> {
// Failed to read version from the Info.plist file of the predefined runtime bundle.
// Try to read it from the "release" file of the predefined runtime directory.
return normalizedVersionFromRuntimeReleaseFile(predefinedRuntimeBundle.homeDir());
});
}).orElseGet(() -> {
return normalizedVersionFromRuntimeReleaseFile(predefinedRuntimePath);
});
} else {
return normalizedVersionFromRuntimeReleaseFile(predefinedRuntimePath);
}
} else {
return Optional.empty();
}
}
private Optional<String> normalizedVersionFromRuntimeReleaseFile(Path runtimeDir) {
return Result.of(() -> {
return RuntimeReleaseFile.loadFromRuntime(runtimeDir).getJavaVersion().toString();
}, Exception.class).value().map(JPackageCommand::normalizeDerivedVersion).map(map -> {
return Objects.requireNonNull(map.get(packageType()));
});
}
public static Map<PackageType, String> normalizeDerivedVersion(String version) {
var dotted = DottedVersion.lazy(version);
var map = new HashMap<PackageType, String>();
// Linux
map.put(PackageType.LINUX_IMAGE, version);
map.put(PackageType.LINUX_DEB, version);
if (dotted.getUnprocessedSuffix().contains("-")) {
map.put(PackageType.LINUX_RPM, dotted.toComponentsString());
} else {
map.put(PackageType.LINUX_RPM, version);
}
// macOS
PackageType.ALL_MAC.forEach(type -> {
map.put(type, dotted.trim(3).pad(1).toComponentsString());
});
// Windows
PackageType.ALL_WINDOWS.forEach(type -> {
DottedVersion ver;
if (dotted.getComponentsCount() < 2) {
ver = dotted.pad(2);
} else {
ver = dotted.trim(4);
}
map.put(type, ver.toComponentsString());
});
map.put(PackageType.IMAGE, Objects.requireNonNull(map.get(PackageType.appImageForOS(PackageType.IMAGE.os()))));
return map;
}
/**
* Sets application version.
* <p>
* Use this method to explicitly set the application version. Normally, the
* application version can be derived from the command line, but sometimes, when
* jpackage derives it from other sources, the {@code JPackageCommand} class
* can't get it correctly. Use this method in these uncommon cases.
*
* @param v the application version or {@code null} to reset previously set
* value
* @return this
*/
public JPackageCommand version(String v) {
verifyMutable();
explicitVersion = v;
return this;
}
public String name() {
@ -1930,6 +2013,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
private Path executeInDirectory;
private Path winMsiLogFile;
private Path unpackedPackageDirectory;
private String explicitVersion;
private Set<ReadOnlyPathAssert> readOnlyPathAsserts = Set.of(ReadOnlyPathAssert.values());
private Set<StandardAssert> standardAsserts = Set.of(StandardAssert.values());
private List<Consumer<Executor.Result>> validators = new ArrayList<>();
@ -1938,7 +2022,9 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
VALUE
}
private static final Map<String, PackageType> PACKAGE_TYPES = Stream.of(PackageType.values()).collect(toMap(PackageType::getType, x -> x));
private static final Map<String, PackageType> PACKAGE_TYPES = Stream.of(PackageType.values()).filter(type -> {
return type.isNative() || type == PackageType.IMAGE;
}).collect(toMap(PackageType::getType, x -> x));
// Set the property to the path of run-time image to speed up
// building app images and platform bundles by avoiding running jlink.
@ -1947,6 +2033,8 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
// `--runtime-image` parameter set.
public static final Path DEFAULT_RUNTIME_IMAGE = Optional.ofNullable(TKit.getConfigProperty("runtime-image")).map(Path::of).orElse(null);
public final static String DEFAULT_VERSION = "1.0";
// [HH:mm:ss.SSS]
private static final Pattern TIMESTAMP_REGEXP = Pattern.compile(
"^\\[\\d\\d:\\d\\d:\\d\\d.\\d\\d\\d\\] ");

View File

@ -48,16 +48,22 @@ public enum PackageType {
LINUX_RPM(".rpm", OperatingSystem.LINUX),
MAC_DMG(".dmg", OperatingSystem.MACOS),
MAC_PKG(".pkg", OperatingSystem.MACOS),
IMAGE;
IMAGE(OperatingSystem.current()),
WIN_IMAGE(OperatingSystem.WINDOWS),
LINUX_IMAGE(OperatingSystem.LINUX),
MAC_IMAGE(OperatingSystem.MACOS),
;
PackageType() {
PackageType(OperatingSystem os) {
this.os = Objects.requireNonNull(os);
type = "app-image";
suffix = null;
supported = true;
enabled = true;
supported = (os == OperatingSystem.current());
enabled = supported;
}
PackageType(String packageName, String bundleSuffix, OperatingSystem os) {
this.os = Objects.requireNonNull(os);
type = Objects.requireNonNull(packageName);
suffix = Objects.requireNonNull(bundleSuffix);
supported = isSupported(new BundlingOperationDescriptor(os, type));
@ -88,10 +94,29 @@ public enum PackageType {
return enabled;
}
public boolean isAppImage() {
return type.equals(IMAGE.type);
}
public boolean isNative() {
return !isAppImage();
}
public String getType() {
return type;
}
public OperatingSystem os() {
return os;
}
public static PackageType appImageForOS(OperatingSystem os) {
Objects.requireNonNull(os);
return Stream.of(LINUX_IMAGE, MAC_IMAGE, WIN_IMAGE).filter(type -> {
return type.os() == os;
}).findFirst().orElseThrow();
}
public static RuntimeException throwSkippedExceptionIfNativePackagingUnavailable() {
if (NATIVE.stream().noneMatch(PackageType::isSupported)) {
TKit.throwSkippedException("None of the native packagers supported in this environment");
@ -115,6 +140,7 @@ public enum PackageType {
return new LinkedHashSet<>(List.of(types));
}
private final OperatingSystem os;
private final String type;
private final String suffix;
private final boolean enabled;
@ -126,6 +152,10 @@ public enum PackageType {
public static final Set<PackageType> NATIVE = Stream.of(LINUX, WINDOWS, MAC)
.flatMap(Collection::stream).collect(Collectors.toUnmodifiableSet());
public static final Set<PackageType> ALL_LINUX = orderedSet(LINUX_IMAGE, LINUX_DEB, LINUX_RPM);
public static final Set<PackageType> ALL_WINDOWS = orderedSet(WIN_IMAGE, WIN_MSI, WIN_EXE);
public static final Set<PackageType> ALL_MAC = orderedSet(MAC_IMAGE, MAC_DMG, MAC_PKG);
private static final class Inner {
private static final Set<String> DISABLED_PACKAGERS = Optional.ofNullable(
TKit.tokenizeConfigProperty("disabledPackagers")).orElse(

View File

@ -22,18 +22,19 @@
*/
package jdk.jpackage.internal.model;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotSame;
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.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Stream;
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;
@ -54,14 +55,6 @@ public class DottedVersionTest {
this(input, type.createVersion, "", expectedComponentCount, input);
}
static TestConfig greedy(String input, int expectedComponentCount, String expectedToComponent) {
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);
}
static TestConfig lazy(String input, String expectedSuffix, int expectedComponentCount, String expectedToComponent) {
return new TestConfig(input, Type.LAZY.createVersion, expectedSuffix, expectedComponentCount, expectedToComponent);
}
@ -74,6 +67,7 @@ public class DottedVersionTest {
assertEquals(cfg.expectedSuffix(), dv.getUnprocessedSuffix());
assertEquals(cfg.expectedComponentCount(), dv.getComponents().length);
assertEquals(cfg.expectedToComponent(), dv.toComponentsString());
assertEquals(dv.toString(), cfg.input());
}
private static List<TestConfig> testValid() {
@ -123,7 +117,7 @@ public class DottedVersionTest {
@ParameterizedTest
@MethodSource
public void testTrim(DottedVersion ver, String expectedStr, int limit) {
public void test_trim(DottedVersion ver, String expectedStr, int limit) {
var expected = DottedVersion.lazy(expectedStr);
var actual = ver.trim(limit);
assertEquals(expected, actual);
@ -136,14 +130,14 @@ public class DottedVersionTest {
}
@ParameterizedTest
@MethodSource
public void testTrimNegative(DottedVersion ver, int limit) {
@MethodSource("test_trim_pad_negative")
public void test_trim_negative(DottedVersion ver, int limit) {
assertThrowsExactly(IllegalArgumentException.class, () -> {
ver.trim(limit);
});
}
private static Stream<Arguments> testTrim() {
private static Stream<Arguments> test_trim() {
var testCases = new ArrayList<Arguments>();
@ -160,15 +154,9 @@ public class DottedVersionTest {
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) {
public void test_pad(DottedVersion ver, String expectedStr, int limit) {
var expected = DottedVersion.lazy(expectedStr);
var actual = ver.pad(limit);
assertEquals(expected, actual);
@ -181,14 +169,14 @@ public class DottedVersionTest {
}
@ParameterizedTest
@MethodSource
public void testPadNegative(DottedVersion ver, int limit) {
@MethodSource("test_trim_pad_negative")
public void test_pad_negative(DottedVersion ver, int limit) {
assertThrowsExactly(IllegalArgumentException.class, () -> {
ver.pad(limit);
});
}
private static Stream<Arguments> testPad() {
private static Stream<Arguments> test_pad() {
var testCases = new ArrayList<Arguments>();
@ -206,7 +194,7 @@ public class DottedVersionTest {
return testCases.stream().map(DottedVersionTest::mapFirstStringToDottedVersion);
}
private static Stream<Arguments> testPadNegative() {
private static Stream<Arguments> test_trim_pad_negative() {
return Stream.of(
Arguments.of("10.5.foo", -1)
).map(DottedVersionTest::mapFirstStringToDottedVersion);

View File

@ -0,0 +1,172 @@
/*
* 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 static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReference;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.Properties;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
class RuntimeReleaseFileTest {
@Test
void test_invalid_input(@TempDir Path workdir) {
assertThrows(IOException.class, () -> {
new RuntimeReleaseFile(workdir);
});
}
@Test
void test_findRawProperty(@TempDir Path workdir) throws IOException {
var releaseFile = new RuntimeReleaseFile(createPropFile(workdir, Map.of("Name", "John", "Company", "\"Acme LTD\"")));
assertEquals(Optional.empty(), releaseFile.findRawProperty("foo"));
assertEquals(Optional.of("John"), releaseFile.findRawProperty("Name"));
assertEquals(Optional.of("\"Acme LTD\""), releaseFile.findRawProperty("Company"));
}
@Test
void test_findProperty(@TempDir Path workdir) throws IOException {
var releaseFile = new RuntimeReleaseFile(createPropFile(workdir, Map.of("Name", "John", "Company", "\"Acme LTD\"")));
assertEquals(Optional.empty(), releaseFile.findProperty("foo"));
assertEquals(Optional.of("John"), releaseFile.findProperty("Name"));
assertEquals(Optional.of("Acme LTD"), releaseFile.findProperty("Company"));
}
@ParameterizedTest
@CsvSource({
"foo, foo",
"\"foo\", foo",
"'foo', 'foo'",
"\"f\"o\"o\", f\"o\"o",
"\"foo, \"foo",
"foo\", foo\"",
})
void test_findProperty(String rawValue, String expectedValue, @TempDir Path workdir) throws IOException {
var releaseFile = new RuntimeReleaseFile(createPropFile(workdir, Map.of("FOO", rawValue)));
assertEquals(expectedValue, releaseFile.findProperty("FOO").orElseThrow());
}
@ParameterizedTest
@CsvSource({
"\"27.1.2\", 27.1.2",
"27.1.2, 27.1.2",
"\"27.1.2-ea\", 27.1.2-ea",
"27.1.2-ea, 27.1.2-ea",
"\"27.1.2+15\", 27.1.2+15",
"27.1.2+15, 27.1.2+15",
})
void test_getJavaVersion(String version, String expectedVersion, @TempDir Path workdir) throws IOException {
var releaseFile = new RuntimeReleaseFile(createPropFileWithValue(workdir, "JAVA_VERSION", version));
final var value = releaseFile.getJavaVersion();
assertEquals(expectedVersion, value.toString());
}
@ParameterizedTest
@CsvSource({
"\"7.1.2+foo\"",
"\"foo\"",
"\"\"",
"7.1.2+foo",
"foo",
"''"
})
void test_getJavaVersion_invalid(String version, @TempDir Path workdir) throws IOException {
var releaseFile = new RuntimeReleaseFile(createPropFileWithValue(workdir, "JAVA_VERSION", version));
var ex = assertThrows(RuntimeException.class, releaseFile::getJavaVersion);
assertFalse(NoSuchElementException.class.isInstance(ex));
}
@Test
void test_without_version(@TempDir Path workdir) throws IOException {
var releaseFile = new RuntimeReleaseFile(createPropFileWithValue(workdir, "JDK_VERSION", "\"27.1.2\""));
assertThrowsExactly(NoSuchElementException.class, releaseFile::getJavaVersion);
}
@Test
void test_getModules(@TempDir Path workdir) throws IOException {
var releaseFile = new RuntimeReleaseFile(createPropFileWithValue(workdir, "MODULES", "foo bar\t buz "));
assertEquals(List.of("foo", "bar", "buz"), releaseFile.getModules());
}
@Test
void test_current() throws IOException {
var releaseFile = new RuntimeReleaseFile(Path.of(System.getProperty("java.home")).resolve("release"));
final var expectedVersion = Runtime.version();
final var actualVersion = releaseFile.getJavaVersion();
assertEquals(expectedVersion.version(), actualVersion.version());
final var expectedModules = ModuleFinder.ofSystem().findAll().stream()
.map(ModuleReference::descriptor).map(ModuleDescriptor::name).sorted().toList();
final var actualModules = releaseFile.getModules().stream().sorted().toList();
assertEquals(expectedModules, actualModules);
}
private Path createPropFileWithValue(Path workdir, String name, String value) {
return createPropFile(workdir, Map.of(name, value));
}
private Path createPropFile(Path workdir, Map<String, String> input) {
Path releaseFile = workdir.resolve("foo");
Properties props = new Properties();
props.putAll(input);
try (Writer writer = Files.newBufferedWriter(releaseFile)) {
props.store(writer, null);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
return releaseFile;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -23,6 +23,7 @@
import static jdk.internal.util.OperatingSystem.LINUX;
import static jdk.internal.util.OperatingSystem.MACOS;
import static jdk.jpackage.test.JPackageCommand.DEFAULT_VERSION;
import static jdk.jpackage.test.TKit.assertFalse;
import static jdk.jpackage.test.TKit.assertTrue;
@ -81,7 +82,16 @@ public class RuntimePackageTest {
@Test
public static void test() {
init().run();
init()
.addInitializer(cmd -> {
// JDK-8357404 enables jpackage to pick a version from the "release" file
// of the predefined runtime when bundling the runtime package.
// This makes the output of this test dependent on the version of the running JDK
// and will be an inconvenience for SQE testing.
// Explicitly specify the package version to fulfill expectations of SQE.
cmd.setArgumentValue("--app-version", DEFAULT_VERSION);
})
.run();
}
@Test(ifOS = MACOS)