8368030: Make package bundlers stateless

Reviewed-by: almatvee
This commit is contained in:
Alexey Semenyuk 2025-09-26 00:10:21 +00:00
parent 648582ab78
commit ca03080c9f
32 changed files with 2412 additions and 1496 deletions

View File

@ -31,6 +31,7 @@ import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
@ -67,7 +68,7 @@ final class DesktopIntegration extends ShellCustomAction {
private static final List<String> REPLACEMENT_STRING_IDS = List.of(
COMMANDS_INSTALL, COMMANDS_UNINSTALL, SCRIPTS, COMMON_SCRIPTS);
private DesktopIntegration(BuildEnv env, LinuxPackage pkg, LinuxLauncher launcher) throws IOException {
private DesktopIntegration(BuildEnv env, LinuxPackage pkg, LinuxLauncher launcher) {
associations = launcher.fileAssociations().stream().map(
LinuxFileAssociation::create).toList();
@ -88,10 +89,14 @@ final class DesktopIntegration extends ShellCustomAction {
// This is additional launcher with explicit `no icon` configuration.
withDesktopFile = false;
} else {
final Path nullPath = null;
if (curIconResource.get().saveToFile(nullPath) != OverridableResource.Source.DefaultResource) {
// This launcher has custom icon configured.
withDesktopFile = true;
try {
if (curIconResource.get().saveToFile((Path)null) != OverridableResource.Source.DefaultResource) {
// This launcher has custom icon configured.
withDesktopFile = true;
}
} catch (IOException ex) {
// Should never happen as `saveToFile((Path)null)` should not perform any actual I/O operations.
throw new UncheckedIOException(ex);
}
}
@ -135,13 +140,13 @@ final class DesktopIntegration extends ShellCustomAction {
return (LinuxLauncher)v;
}).filter(l -> {
return toRequest(l.shortcut()).orElse(true);
}).map(toFunction(l -> {
}).map(l -> {
return new DesktopIntegration(env, pkg, l);
})).toList();
}).toList();
}
}
static ShellCustomAction create(BuildEnv env, Package pkg) throws IOException {
static ShellCustomAction create(BuildEnv env, Package pkg) {
if (pkg.isRuntimeInstaller()) {
return ShellCustomAction.nop(REPLACEMENT_STRING_IDS);
}

View File

@ -25,362 +25,19 @@
package jdk.jpackage.internal;
import jdk.jpackage.internal.model.LinuxPackage;
import jdk.jpackage.internal.model.PackagerException;
import jdk.jpackage.internal.model.ConfigException;
import jdk.jpackage.internal.model.LinuxDebPackage;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jdk.internal.util.OperatingSystem;
import jdk.jpackage.internal.model.AppImageLayout;
import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction;
import static jdk.jpackage.internal.model.StandardPackageType.LINUX_DEB;
import jdk.jpackage.internal.model.LinuxDebPackage;
import jdk.jpackage.internal.model.PackagerException;
import jdk.jpackage.internal.model.StandardPackageType;
import jdk.jpackage.internal.util.Result;
public class LinuxDebBundler extends LinuxPackageBundler {
private static final String TOOL_DPKG_DEB = "dpkg-deb";
private static final String TOOL_DPKG = "dpkg";
private static final String TOOL_FAKEROOT = "fakeroot";
public LinuxDebBundler() {
super(LinuxFromParams.DEB_PACKAGE);
}
@Override
protected void doValidate(BuildEnv env, LinuxPackage pkg) throws ConfigException {
// Show warning if license file is missing
if (pkg.licenseFile().isEmpty()) {
Log.verbose(I18N.getString("message.debs-like-licenses"));
}
}
@Override
protected List<ToolValidator> getToolValidators() {
return Stream.of(TOOL_DPKG_DEB, TOOL_DPKG, TOOL_FAKEROOT).map(
ToolValidator::new).toList();
}
@Override
protected void createConfigFiles(Map<String, String> replacementData,
BuildEnv env, LinuxPackage pkg) throws IOException {
prepareProjectConfig(replacementData, env, pkg);
adjustPermissionsRecursive(env.appImageDir());
}
@Override
protected Path buildPackageBundle(BuildEnv env, LinuxPackage pkg,
Path outputParentDir) throws PackagerException, IOException {
return buildDeb(env, pkg, outputParentDir);
}
private static final Pattern PACKAGE_NAME_REGEX = Pattern.compile("^(^\\S+):");
@Override
protected void initLibProvidersLookup(LibProvidersLookup libProvidersLookup) {
libProvidersLookup.setPackageLookup(file -> {
Path realPath = file.toRealPath();
try {
// Try the real path first as it works better on newer Ubuntu versions
return findProvidingPackages(realPath);
} catch (IOException ex) {
// Try the default path if differ
if (!realPath.toString().equals(file.toString())) {
return findProvidingPackages(file);
} else {
throw ex;
}
}
});
}
private static Stream<String> findProvidingPackages(Path file) 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.
// Even for full paths multiple package names can be returned as
// it is OK for multiple packages to provide the same file. `/opt`
// directory is such an example. So we have to deal with multiple
// packages per file situation.
//
// E.g.: `dpkg -S libc.so.6` command reports three packages:
// libc6-x32: /libx32/libc.so.6
// libc6:amd64: /lib/x86_64-linux-gnu/libc.so.6
// libc6-i386: /lib32/libc.so.6
// `:amd64` is architecture suffix and can (should) be dropped.
// Still need to decide what package to choose from three.
// libc6-x32 and libc6-i386 both depend on libc6:
// $ dpkg -s libc6-x32
// Package: libc6-x32
// Status: install ok installed
// Priority: optional
// Section: libs
// Installed-Size: 10840
// Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
// Architecture: amd64
// Source: glibc
// Version: 2.23-0ubuntu10
// Depends: libc6 (= 2.23-0ubuntu10)
//
// We can dive into tracking dependencies, but this would be overly
// complicated.
//
// For simplicity lets consider the following rules:
// 1. If there is one item in `dpkg -S` output, accept it.
// 2. If there are multiple items in `dpkg -S` output and there is at
// least one item with the default arch suffix (DEB_ARCH),
// accept only these items.
// 3. If there are multiple items in `dpkg -S` output and there are
// no with the default arch suffix (DEB_ARCH), accept all items.
// So lets use this heuristics: don't accept packages for whom
// `dpkg -p` command fails.
// 4. Arch suffix should be stripped from accepted package names.
//
Set<String> archPackages = new HashSet<>();
Set<String> otherPackages = new HashSet<>();
var debArch = LinuxPackageArch.getValue(LINUX_DEB);
Executor.of(TOOL_DPKG, "-S", file.toString())
.saveOutput(true).executeExpectSuccess()
.getOutput().forEach(line -> {
Matcher matcher = PACKAGE_NAME_REGEX.matcher(line);
if (matcher.find()) {
String name = matcher.group(1);
if (name.endsWith(":" + debArch)) {
// Strip arch suffix
name = name.substring(0,
name.length() - (debArch.length() + 1));
archPackages.add(name);
} else {
otherPackages.add(name);
}
}
});
if (!archPackages.isEmpty()) {
return archPackages.stream();
}
return otherPackages.stream();
}
@Override
protected List<ConfigException> verifyOutputBundle(BuildEnv env, LinuxPackage pkg,
Path packageBundle) {
List<ConfigException> errors = new ArrayList<>();
String controlFileName = "control";
List<PackageProperty> properties = List.of(
new PackageProperty("Package", pkg.packageName(),
"APPLICATION_PACKAGE", controlFileName),
new PackageProperty("Version", ((LinuxDebPackage)pkg).versionWithRelease(),
"APPLICATION_VERSION_WITH_RELEASE",
controlFileName),
new PackageProperty("Architecture", pkg.arch(), "APPLICATION_ARCH", controlFileName));
List<String> cmdline = new ArrayList<>(List.of(TOOL_DPKG_DEB, "-f",
packageBundle.toString()));
properties.forEach(property -> cmdline.add(property.name));
try {
Map<String, String> actualValues = Executor.of(cmdline.toArray(String[]::new))
.saveOutput(true)
.executeExpectSuccess()
.getOutput().stream()
.map(line -> line.split(":\\s+", 2))
.collect(Collectors.toMap(
components -> components[0],
components -> components[1]));
properties.forEach(property -> errors.add(property.verifyValue(
actualValues.get(property.name))));
} catch (IOException ex) {
// Ignore error as it is not critical. Just report it.
Log.verbose(ex);
}
return errors;
}
/*
* set permissions with a string like "rwxr-xr-x"
*
* This cannot be directly backport to 22u which is built with 1.6
*/
private void setPermissions(Path file, String permissions) {
Set<PosixFilePermission> filePermissions =
PosixFilePermissions.fromString(permissions);
try {
if (Files.exists(file)) {
Files.setPosixFilePermissions(file, filePermissions);
}
} catch (IOException ex) {
Log.error(ex.getMessage());
Log.verbose(ex);
}
}
public static boolean isDebian() {
// we are just going to run "dpkg -s coreutils" and assume Debian
// or deritive if no error is returned.
try {
Executor.of(TOOL_DPKG, "-s", "coreutils").executeExpectSuccess();
return true;
} catch (IOException e) {
// just fall thru
}
return false;
}
private void adjustPermissionsRecursive(Path dir) throws IOException {
Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file,
BasicFileAttributes attrs)
throws IOException {
if (file.endsWith(".so") || !Files.isExecutable(file)) {
setPermissions(file, "rw-r--r--");
} else if (Files.isExecutable(file)) {
setPermissions(file, "rwxr-xr-x");
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException e)
throws IOException {
if (e == null) {
setPermissions(dir, "rwxr-xr-x");
return FileVisitResult.CONTINUE;
} else {
// directory iteration failed
throw e;
}
}
});
}
private class DebianFile {
DebianFile(Path dstFilePath, String comment) {
this.dstFilePath = dstFilePath;
this.comment = comment;
}
DebianFile setExecutable() {
permissions = "rwxr-xr-x";
return this;
}
void create(Map<String, String> data, Function<String, OverridableResource> resourceFactory)
throws IOException {
resourceFactory.apply("template." + dstFilePath.getFileName().toString())
.setCategory(I18N.getString(comment))
.setSubstitutionData(data)
.saveToFile(dstFilePath);
if (permissions != null) {
setPermissions(dstFilePath, permissions);
}
}
private final Path dstFilePath;
private final String comment;
private String permissions;
}
private void prepareProjectConfig(Map<String, String> data, BuildEnv env, LinuxPackage pkg) throws IOException {
Path configDir = env.appImageDir().resolve("DEBIAN");
List<DebianFile> debianFiles = new ArrayList<>();
debianFiles.add(new DebianFile(
configDir.resolve("control"),
"resource.deb-control-file"));
debianFiles.add(new DebianFile(
configDir.resolve("preinst"),
"resource.deb-preinstall-script").setExecutable());
debianFiles.add(new DebianFile(
configDir.resolve("prerm"),
"resource.deb-prerm-script").setExecutable());
debianFiles.add(new DebianFile(
configDir.resolve("postinst"),
"resource.deb-postinstall-script").setExecutable());
debianFiles.add(new DebianFile(
configDir.resolve("postrm"),
"resource.deb-postrm-script").setExecutable());
((LinuxDebPackage)pkg).relativeCopyrightFilePath().ifPresent(copyrightFile -> {
debianFiles.add(new DebianFile(env.appImageDir().resolve(copyrightFile),
"resource.copyright-file"));
});
for (DebianFile debianFile : debianFiles) {
debianFile.create(data, env::createResource);
}
}
@Override
protected Map<String, String> createReplacementData(BuildEnv env, LinuxPackage pkg) throws IOException {
Map<String, String> data = new HashMap<>();
String licenseText = pkg.licenseFile().map(toFunction(Files::readString)).orElse("Unknown");
data.put("APPLICATION_MAINTAINER", ((LinuxDebPackage) pkg).maintainer());
data.put("APPLICATION_SECTION", pkg.category().orElseThrow());
data.put("APPLICATION_COPYRIGHT", pkg.app().copyright());
data.put("APPLICATION_LICENSE_TEXT", licenseText);
data.put("APPLICATION_ARCH", pkg.arch());
data.put("APPLICATION_INSTALLED_SIZE", Long.toString(
AppImageLayout.toPathGroup(env.appImageLayout()).sizeInBytes() >> 10));
data.put("APPLICATION_HOMEPAGE", pkg.aboutURL().map(
value -> "Homepage: " + value).orElse(""));
data.put("APPLICATION_VERSION_WITH_RELEASE", ((LinuxDebPackage) pkg).versionWithRelease());
return data;
}
private Path buildDeb(BuildEnv env, LinuxPackage pkg, Path outdir) throws IOException {
Path outFile = outdir.resolve(pkg.packageFileNameWithSuffix());
Log.verbose(I18N.format("message.outputting-to-location", outFile.toAbsolutePath()));
List<String> cmdline = new ArrayList<>();
cmdline.addAll(List.of(TOOL_FAKEROOT, TOOL_DPKG_DEB));
if (Log.isVerbose()) {
cmdline.add("--verbose");
}
cmdline.addAll(List.of("-b", env.appImageDir().toString(),
outFile.toAbsolutePath().toString()));
// run dpkg
RetryExecutor.retryOnKnownErrorMessage(
"semop(1): encountered an error: Invalid argument").execute(
cmdline.toArray(String[]::new));
Log.verbose(I18N.format("message.output-to-location", outFile.toAbsolutePath()));
return outFile;
}
@Override
public String getName() {
return I18N.getString("deb.bundler.name");
@ -392,12 +49,28 @@ public class LinuxDebBundler extends LinuxPackageBundler {
}
@Override
public boolean supported(boolean runtimeInstaller) {
return OperatingSystem.isLinux() && (new ToolValidator(TOOL_DPKG_DEB).validate() == null);
public Path execute(Map<String, ? super Object> params, Path outputParentDir) throws PackagerException {
return Packager.<LinuxDebPackage>build().outputDir(outputParentDir)
.pkg(LinuxFromParams.DEB_PACKAGE.fetchFrom(params))
.env(BuildEnvFromParams.BUILD_ENV.fetchFrom(params))
.pipelineBuilderMutatorFactory((env, pkg, outputDir) -> {
return new LinuxDebPackager(env, pkg, outputDir, sysEnv.orElseThrow());
}).execute(LinuxPackagingPipeline.build());
}
@Override
protected Result<LinuxDebSystemEnvironment> sysEnv() {
return sysEnv;
}
@Override
public boolean isDefault() {
return isDebian();
return sysEnv.value()
.map(LinuxSystemEnvironment::nativePackageType)
.map(StandardPackageType.LINUX_DEB::equals)
.orElse(false);
}
private final Result<LinuxDebSystemEnvironment> sysEnv = LinuxDebSystemEnvironment.create(SYS_ENV);
}

View File

@ -0,0 +1,344 @@
/*
* Copyright (c) 2012, 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. 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 static jdk.jpackage.internal.model.StandardPackageType.LINUX_DEB;
import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jdk.jpackage.internal.PackagingPipeline.PackageTaskID;
import jdk.jpackage.internal.PackagingPipeline.PrimaryTaskID;
import jdk.jpackage.internal.model.AppImageLayout;
import jdk.jpackage.internal.model.ConfigException;
import jdk.jpackage.internal.model.LinuxDebPackage;
final class LinuxDebPackager extends LinuxPackager<LinuxDebPackage> {
LinuxDebPackager(BuildEnv env, LinuxDebPackage pkg, Path outputDir, LinuxDebSystemEnvironment sysEnv) {
super(env, pkg, outputDir, sysEnv);
this.sysEnv = Objects.requireNonNull(sysEnv);
}
@Override
protected void createConfigFiles(Map<String, String> replacementData) throws IOException {
prepareProjectConfig(replacementData);
adjustPermissionsRecursive();
}
@Override
protected void initLibProvidersLookup(LibProvidersLookup libProvidersLookup) {
libProvidersLookup.setPackageLookup(file -> {
Path realPath = file.toRealPath();
try {
// Try the real path first as it works better on newer Ubuntu versions
return findProvidingPackages(realPath, sysEnv.dpkg());
} catch (IOException ex) {
// Try the default path if differ
if (!realPath.equals(file)) {
return findProvidingPackages(file, sysEnv.dpkg());
} else {
throw ex;
}
}
});
}
@Override
protected List<? extends Exception> findErrorsInOutputPackage() throws IOException {
List<ConfigException> errors = new ArrayList<>();
var controlFileName = "control";
List<PackageProperty> properties = List.of(
new PackageProperty("Package", pkg.packageName(),
"APPLICATION_PACKAGE", controlFileName),
new PackageProperty("Version", pkg.versionWithRelease(),
"APPLICATION_VERSION_WITH_RELEASE",
controlFileName),
new PackageProperty("Architecture", pkg.arch(), "APPLICATION_ARCH", controlFileName));
List<String> cmdline = new ArrayList<>(List.of(
sysEnv.dpkgdeb().toString(), "-f", outputPackageFile().toString()));
properties.forEach(property -> cmdline.add(property.name));
Map<String, String> actualValues = Executor.of(cmdline.toArray(String[]::new))
.saveOutput(true)
.executeExpectSuccess()
.getOutput().stream()
.map(line -> line.split(":\\s+", 2))
.collect(Collectors.toMap(
components -> components[0],
components -> components[1]));
for (var property : properties) {
Optional.ofNullable(property.verifyValue(actualValues.get(property.name))).ifPresent(errors::add);
}
return errors;
}
@Override
protected Map<String, String> createReplacementData() throws IOException {
Map<String, String> data = new HashMap<>();
String licenseText = pkg.licenseFile().map(toFunction(Files::readString)).orElse("Unknown");
data.put("APPLICATION_MAINTAINER", pkg.maintainer());
data.put("APPLICATION_SECTION", pkg.category().orElseThrow());
data.put("APPLICATION_COPYRIGHT", pkg.app().copyright());
data.put("APPLICATION_LICENSE_TEXT", licenseText);
data.put("APPLICATION_ARCH", pkg.arch());
data.put("APPLICATION_INSTALLED_SIZE", Long.toString(
AppImageLayout.toPathGroup(env.appImageLayout()).sizeInBytes() >> 10));
data.put("APPLICATION_HOMEPAGE", pkg.aboutURL().map(
value -> "Homepage: " + value).orElse(""));
data.put("APPLICATION_VERSION_WITH_RELEASE", pkg.versionWithRelease());
return data;
}
@Override
protected void buildPackage() throws IOException {
Path debFile = outputPackageFile();
Log.verbose(I18N.format("message.outputting-to-location", debFile.toAbsolutePath()));
List<String> cmdline = new ArrayList<>();
Stream.of(sysEnv.fakeroot(), sysEnv.dpkgdeb()).map(Path::toString).forEach(cmdline::add);
if (Log.isVerbose()) {
cmdline.add("--verbose");
}
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));
Log.verbose(I18N.format("message.output-to-location", debFile.toAbsolutePath()));
}
@Override
public void accept(PackagingPipeline.Builder pipelineBuilder) {
super.accept(pipelineBuilder);
// Build deb config files after app image contents are ready because
// it calculates the size of the image and saves the value in one of the config files.
pipelineBuilder.configuredTasks().filter(task -> {
return PackageTaskID.CREATE_CONFIG_FILES.equals(task.task());
}).findFirst().orElseThrow()
.addDependencies(PrimaryTaskID.BUILD_APPLICATION_IMAGE, PrimaryTaskID.COPY_APP_IMAGE)
.add();
}
private void adjustPermissionsRecursive() throws IOException {
Files.walkFileTree(env.appImageDir(), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (file.endsWith(".so") || !Files.isExecutable(file)) {
Files.setPosixFilePermissions(file, SO_PERMISSIONS);
} else if (Files.isExecutable(file)) {
Files.setPosixFilePermissions(file, EXECUTABLE_PERMISSIONS);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException {
if (e == null) {
Files.setPosixFilePermissions(dir, FOLDER_PERMISSIONS);
return FileVisitResult.CONTINUE;
} else {
// directory iteration failed
throw e;
}
}
});
}
private void prepareProjectConfig(Map<String, String> data) throws IOException {
Path configDir = env.appImageDir().resolve("DEBIAN");
List<DebianFile> debianFiles = new ArrayList<>();
debianFiles.add(new DebianFile(
configDir.resolve("control"),
"resource.deb-control-file"));
debianFiles.add(new DebianFile(
configDir.resolve("preinst"),
"resource.deb-preinstall-script").setExecutable());
debianFiles.add(new DebianFile(
configDir.resolve("prerm"),
"resource.deb-prerm-script").setExecutable());
debianFiles.add(new DebianFile(
configDir.resolve("postinst"),
"resource.deb-postinstall-script").setExecutable());
debianFiles.add(new DebianFile(
configDir.resolve("postrm"),
"resource.deb-postrm-script").setExecutable());
pkg.relativeCopyrightFilePath().ifPresent(copyrightFile -> {
debianFiles.add(new DebianFile(env.appImageDir().resolve(copyrightFile),
"resource.copyright-file"));
});
for (DebianFile debianFile : debianFiles) {
debianFile.create(data, env::createResource);
}
}
private static Stream<String> findProvidingPackages(Path file, Path dpkg) 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.
// Even for full paths multiple package names can be returned as
// it is OK for multiple packages to provide the same file. `/opt`
// directory is such an example. So we have to deal with multiple
// packages per file situation.
//
// E.g.: `dpkg -S libc.so.6` command reports three packages:
// libc6-x32: /libx32/libc.so.6
// libc6:amd64: /lib/x86_64-linux-gnu/libc.so.6
// libc6-i386: /lib32/libc.so.6
// `:amd64` is architecture suffix and can (should) be dropped.
// Still need to decide what package to choose from three.
// libc6-x32 and libc6-i386 both depend on libc6:
// $ dpkg -s libc6-x32
// Package: libc6-x32
// Status: install ok installed
// Priority: optional
// Section: libs
// Installed-Size: 10840
// Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
// Architecture: amd64
// Source: glibc
// Version: 2.23-0ubuntu10
// Depends: libc6 (= 2.23-0ubuntu10)
//
// We can dive into tracking dependencies, but this would be overly
// complicated.
//
// For simplicity lets consider the following rules:
// 1. If there is one item in `dpkg -S` output, accept it.
// 2. If there are multiple items in `dpkg -S` output and there is at
// least one item with the default arch suffix (DEB_ARCH),
// accept only these items.
// 3. If there are multiple items in `dpkg -S` output and there are
// no with the default arch suffix (DEB_ARCH), accept all items.
// So lets use this heuristics: don't accept packages for whom
// `dpkg -p` command fails.
// 4. Arch suffix should be stripped from accepted package names.
//
Set<String> archPackages = new HashSet<>();
Set<String> otherPackages = new HashSet<>();
var debArch = LinuxPackageArch.getValue(LINUX_DEB);
Executor.of(dpkg.toString(), "-S", file.toString())
.saveOutput(true).executeExpectSuccess()
.getOutput().forEach(line -> {
Matcher matcher = PACKAGE_NAME_REGEX.matcher(line);
if (matcher.find()) {
String name = matcher.group(1);
if (name.endsWith(":" + debArch)) {
// Strip arch suffix
name = name.substring(0,
name.length() - (debArch.length() + 1));
archPackages.add(name);
} else {
otherPackages.add(name);
}
}
});
if (!archPackages.isEmpty()) {
return archPackages.stream();
}
return otherPackages.stream();
}
private static final class DebianFile {
DebianFile(Path dstFilePath, String comment) {
this.dstFilePath = Objects.requireNonNull(dstFilePath);
this.comment = Objects.requireNonNull(comment);
}
DebianFile setExecutable() {
permissions = EXECUTABLE_PERMISSIONS;
return this;
}
void create(Map<String, String> data, Function<String, OverridableResource> resourceFactory)
throws IOException {
resourceFactory.apply("template." + dstFilePath.getFileName().toString())
.setCategory(I18N.getString(comment))
.setSubstitutionData(data)
.saveToFile(dstFilePath);
if (permissions != null) {
Files.setPosixFilePermissions(dstFilePath, permissions);
}
}
private final Path dstFilePath;
private final String comment;
private Set<PosixFilePermission> permissions;
}
private final LinuxDebSystemEnvironment sysEnv;
private static final Pattern PACKAGE_NAME_REGEX = Pattern.compile("^(^\\S+):");
private static final Set<PosixFilePermission> EXECUTABLE_PERMISSIONS = PosixFilePermissions.fromString("rwxr-xr-x");
private static final Set<PosixFilePermission> FOLDER_PERMISSIONS = PosixFilePermissions.fromString("rwxr-xr-x");
private static final Set<PosixFilePermission> SO_PERMISSIONS = PosixFilePermissions.fromString("rw-r--r--");
}

View File

@ -0,0 +1,36 @@
/*
* 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. 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 static jdk.jpackage.internal.LinuxSystemEnvironment.mixin;
import jdk.jpackage.internal.util.Result;
public interface LinuxDebSystemEnvironment extends LinuxSystemEnvironment, LinuxDebSystemEnvironmentMixin {
static Result<LinuxDebSystemEnvironment> create(Result<LinuxSystemEnvironment> base) {
return mixin(LinuxDebSystemEnvironment.class, base, LinuxDebSystemEnvironmentMixin::create);
}
}

View File

@ -0,0 +1,59 @@
/*
* 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. 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.nio.file.Path;
import java.util.Objects;
import java.util.stream.Stream;
import jdk.jpackage.internal.util.Result;
public interface LinuxDebSystemEnvironmentMixin {
Path dpkg();
Path dpkgdeb();
Path fakeroot();
record Stub(Path dpkg, Path dpkgdeb, Path fakeroot) implements LinuxDebSystemEnvironmentMixin {
}
static Result<LinuxDebSystemEnvironmentMixin> create() {
final var errors = Stream.of(Internal.TOOL_DPKG_DEB, Internal.TOOL_DPKG, Internal.TOOL_FAKEROOT)
.map(ToolValidator::new)
.map(ToolValidator::validate)
.filter(Objects::nonNull)
.toList();
if (errors.isEmpty()) {
return Result.ofValue(new Stub(Internal.TOOL_DPKG, Internal.TOOL_DPKG_DEB, Internal.TOOL_FAKEROOT));
} else {
return Result.ofErrors(errors);
}
}
static final class Internal {
private static final Path TOOL_DPKG_DEB = Path.of("dpkg-deb");
private static final Path TOOL_DPKG = Path.of("dpkg");
private static final Path TOOL_FAKEROOT = Path.of("fakeroot");
}
}

View File

@ -39,9 +39,10 @@ import java.io.IOException;
import java.util.Map;
import jdk.jpackage.internal.model.ConfigException;
import jdk.jpackage.internal.model.LinuxApplication;
import jdk.jpackage.internal.model.LinuxDebPackage;
import jdk.jpackage.internal.model.LinuxLauncher;
import jdk.jpackage.internal.model.LinuxLauncherMixin;
import jdk.jpackage.internal.model.LinuxPackage;
import jdk.jpackage.internal.model.LinuxRpmPackage;
import jdk.jpackage.internal.model.StandardPackageType;
final class LinuxFromParams {
@ -76,7 +77,7 @@ final class LinuxFromParams {
return pkgBuilder;
}
private static LinuxPackage createLinuxRpmPackage(
private static LinuxRpmPackage createLinuxRpmPackage(
Map<String, ? super Object> params) throws ConfigException, IOException {
final var superPkgBuilder = createLinuxPackageBuilder(params, LINUX_RPM);
@ -88,7 +89,7 @@ final class LinuxFromParams {
return pkgBuilder.create();
}
private static LinuxPackage createLinuxDebPackage(
private static LinuxDebPackage createLinuxDebPackage(
Map<String, ? super Object> params) throws ConfigException, IOException {
final var superPkgBuilder = createLinuxPackageBuilder(params, LINUX_DEB);
@ -97,16 +98,23 @@ final class LinuxFromParams {
MAINTAINER_EMAIL.copyInto(params, pkgBuilder::maintainerEmail);
return pkgBuilder.create();
final var pkg = pkgBuilder.create();
// Show warning if license file is missing
if (pkg.licenseFile().isEmpty()) {
Log.verbose(I18N.getString("message.debs-like-licenses"));
}
return pkg;
}
static final BundlerParamInfo<LinuxApplication> APPLICATION = createApplicationBundlerParam(
LinuxFromParams::createLinuxApplication);
static final BundlerParamInfo<LinuxPackage> RPM_PACKAGE = createPackageBundlerParam(
static final BundlerParamInfo<LinuxRpmPackage> RPM_PACKAGE = createPackageBundlerParam(
LinuxFromParams::createLinuxRpmPackage);
static final BundlerParamInfo<LinuxPackage> DEB_PACKAGE = createPackageBundlerParam(
static final BundlerParamInfo<LinuxDebPackage> DEB_PACKAGE = createPackageBundlerParam(
LinuxFromParams::createLinuxDebPackage);
private static final BundlerParamInfo<String> LINUX_SHORTCUT_HINT = createStringBundlerParam(

View File

@ -38,7 +38,7 @@ import jdk.jpackage.internal.model.Package;
*/
public final class LinuxLaunchersAsServices extends UnixLaunchersAsServices {
private LinuxLaunchersAsServices(BuildEnv env, Package pkg) throws IOException {
private LinuxLaunchersAsServices(BuildEnv env, Package pkg) {
super(env.appImageDir(), pkg.app(), REQUIRED_PACKAGES, launcher -> {
return new LauncherImpl(env, pkg, launcher);
});
@ -58,7 +58,7 @@ public final class LinuxLaunchersAsServices extends UnixLaunchersAsServices {
return data;
}
static ShellCustomAction create(BuildEnv env, Package pkg) throws IOException {
static ShellCustomAction create(BuildEnv env, Package pkg) {
if (pkg.isRuntimeInstaller()) {
return ShellCustomAction.nop(LINUX_REPLACEMENT_STRING_IDS);
}

View File

@ -24,33 +24,16 @@
*/
package jdk.jpackage.internal;
import java.io.IOException;
import java.nio.file.Path;
import java.text.MessageFormat;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Stream;
import jdk.jpackage.internal.PackagingPipeline.PackageBuildEnv;
import jdk.jpackage.internal.PackagingPipeline.PackageTaskID;
import jdk.jpackage.internal.PackagingPipeline.PrimaryTaskID;
import jdk.jpackage.internal.model.AppImageLayout;
import jdk.jpackage.internal.model.ConfigException;
import jdk.jpackage.internal.model.LinuxDebPackage;
import jdk.jpackage.internal.model.LinuxPackage;
import jdk.jpackage.internal.model.Package;
import jdk.jpackage.internal.model.PackagerException;
import jdk.jpackage.internal.util.Result;
abstract class LinuxPackageBundler extends AbstractBundler {
LinuxPackageBundler(BundlerParamInfo<? extends LinuxPackage> pkgParam) {
this.pkgParam = pkgParam;
customActions = List.of(new CustomActionInstance(
DesktopIntegration::create), new CustomActionInstance(
LinuxLaunchersAsServices::create));
this.pkgParam = Objects.requireNonNull(pkgParam);
}
@Override
@ -58,39 +41,32 @@ abstract class LinuxPackageBundler extends AbstractBundler {
throws ConfigException {
// Order is important!
LinuxPackage pkg = pkgParam.fetchFrom(params);
var env = BuildEnvFromParams.BUILD_ENV.fetchFrom(params);
pkgParam.fetchFrom(params);
BuildEnvFromParams.BUILD_ENV.fetchFrom(params);
for (var validator: getToolValidators()) {
ConfigException ex = validator.validate();
if (ex != null) {
throw ex;
}
LinuxSystemEnvironment sysEnv;
try {
sysEnv = sysEnv().orElseThrow();
} catch (RuntimeException ex) {
throw ConfigException.rethrowConfigException(ex);
}
if (!isDefault()) {
withFindNeededPackages = false;
Log.verbose(MessageFormat.format(I18N.getString(
"message.not-default-bundler-no-dependencies-lookup"),
Log.verbose(I18N.format(
"message.not-default-bundler-no-dependencies-lookup",
getName()));
} else {
withFindNeededPackages = LibProvidersLookup.supported();
if (!withFindNeededPackages) {
final String advice;
if ("deb".equals(getID())) {
advice = "message.deb-ldd-not-available.advice";
} else {
advice = "message.rpm-ldd-not-available.advice";
}
// Let user know package dependencies will not be generated.
Log.error(String.format("%s\n%s", I18N.getString(
"message.ldd-not-available"), I18N.getString(advice)));
} else if (!sysEnv.soLookupAvailable()) {
final String advice;
if ("deb".equals(getID())) {
advice = "message.deb-ldd-not-available.advice";
} else {
advice = "message.rpm-ldd-not-available.advice";
}
// Let user know package dependencies will not be generated.
Log.error(String.format("%s\n%s", I18N.getString(
"message.ldd-not-available"), I18N.getString(advice)));
}
// Packaging specific validation
doValidate(env, pkg);
return true;
}
@ -100,148 +76,13 @@ abstract class LinuxPackageBundler extends AbstractBundler {
}
@Override
public final Path execute(Map<String, ? super Object> params,
Path outputParentDir) throws PackagerException {
IOUtils.writableOutputDir(outputParentDir);
// Order is important!
final LinuxPackage pkg = pkgParam.fetchFrom(params);
final var env = BuildEnvFromParams.BUILD_ENV.fetchFrom(params);
final var pipelineBuilder = LinuxPackagingPipeline.build()
.excludeDirFromCopying(outputParentDir)
.task(PackageTaskID.CREATE_PACKAGE_FILE)
.packageAction(this::buildPackage)
.add();
final var createConfigFilesTaskBuilder = pipelineBuilder
.task(PackageTaskID.CREATE_CONFIG_FILES)
.packageAction(this::buildConfigFiles);
if (pkg instanceof LinuxDebPackage) {
// Build deb config files after app image contents are ready because
// it calculates the size of the image and saves the value in one of the config files.
createConfigFilesTaskBuilder.addDependencies(PrimaryTaskID.BUILD_APPLICATION_IMAGE, PrimaryTaskID.COPY_APP_IMAGE);
}
createConfigFilesTaskBuilder.add();
pipelineBuilder.create().execute(env, pkg, outputParentDir);
return outputParentDir.resolve(pkg.packageFileNameWithSuffix()).toAbsolutePath();
public boolean supported(boolean runtimeInstaller) {
return sysEnv().hasValue();
}
private void buildConfigFiles(PackageBuildEnv<LinuxPackage, AppImageLayout> env) throws PackagerException, IOException {
for (var ca : customActions) {
ca.init(env.env(), env.pkg());
}
Map<String, String> data = createDefaultReplacementData(env.env(), env.pkg());
for (var ca : customActions) {
ShellCustomAction.mergeReplacementData(data, ca.instance.create());
}
data.putAll(createReplacementData(env.env(), env.pkg()));
createConfigFiles(Collections.unmodifiableMap(data), env.env(), env.pkg());
}
private void buildPackage(PackageBuildEnv<LinuxPackage, AppImageLayout> env) throws PackagerException, IOException {
Path packageBundle = buildPackageBundle(env.env(), env.pkg(), env.outputDir());
verifyOutputBundle(env.env(), env.pkg(), packageBundle).stream()
.filter(Objects::nonNull)
.forEachOrdered(ex -> {
Log.verbose(ex.getLocalizedMessage());
Log.verbose(ex.getAdvice());
});
}
private List<String> getListOfNeededPackages(BuildEnv env) throws IOException {
final List<String> caPackages = customActions.stream()
.map(ca -> ca.instance)
.map(ShellCustomAction::requiredPackages)
.flatMap(List::stream).toList();
final List<String> neededLibPackages;
if (withFindNeededPackages) {
LibProvidersLookup lookup = new LibProvidersLookup();
initLibProvidersLookup(lookup);
neededLibPackages = lookup.execute(env.appImageDir());
} else {
neededLibPackages = Collections.emptyList();
Log.info(I18N.getString("warning.foreign-app-image"));
}
// Merge all package lists together.
// Filter out empty names, sort and remove duplicates.
List<String> result = Stream.of(caPackages, neededLibPackages).flatMap(
List::stream).filter(Predicate.not(String::isEmpty)).sorted().distinct().toList();
Log.verbose(String.format("Required packages: %s", result));
return result;
}
private Map<String, String> createDefaultReplacementData(BuildEnv env, LinuxPackage pkg) throws IOException {
Map<String, String> data = new HashMap<>();
data.put("APPLICATION_PACKAGE", pkg.packageName());
data.put("APPLICATION_VENDOR", pkg.app().vendor());
data.put("APPLICATION_VERSION", pkg.version());
data.put("APPLICATION_DESCRIPTION", pkg.description());
String defaultDeps = String.join(", ", getListOfNeededPackages(env));
String customDeps = pkg.additionalDependencies().orElse("");
if (!customDeps.isEmpty() && !defaultDeps.isEmpty()) {
customDeps = ", " + customDeps;
}
data.put("PACKAGE_DEFAULT_DEPENDENCIES", defaultDeps);
data.put("PACKAGE_CUSTOM_DEPENDENCIES", customDeps);
return data;
}
protected abstract List<ConfigException> verifyOutputBundle(
BuildEnv env, LinuxPackage pkg, Path packageBundle);
protected abstract void initLibProvidersLookup(LibProvidersLookup libProvidersLookup);
protected abstract List<ToolValidator> getToolValidators();
protected abstract void doValidate(BuildEnv env, LinuxPackage pkg)
throws ConfigException;
protected abstract Map<String, String> createReplacementData(
BuildEnv env, LinuxPackage pkg) throws IOException;
protected abstract void createConfigFiles(
Map<String, String> replacementData,
BuildEnv env, LinuxPackage pkg) throws IOException;
protected abstract Path buildPackageBundle(
BuildEnv env, LinuxPackage pkg, Path outputParentDir) throws
PackagerException, IOException;
protected abstract Result<? extends LinuxSystemEnvironment> sysEnv();
private final BundlerParamInfo<? extends LinuxPackage> pkgParam;
private boolean withFindNeededPackages;
private final List<CustomActionInstance> customActions;
private static final class CustomActionInstance {
CustomActionInstance(ShellCustomActionFactory factory) {
this.factory = factory;
}
void init(BuildEnv env, Package pkg) throws IOException {
instance = factory.create(env, pkg);
Objects.requireNonNull(instance);
}
private final ShellCustomActionFactory factory;
ShellCustomAction instance;
}
static final Result<LinuxSystemEnvironment> SYS_ENV = LinuxSystemEnvironment.create();
}

View File

@ -0,0 +1,184 @@
/*
* 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. 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.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.Objects;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Stream;
import jdk.jpackage.internal.PackagingPipeline.PackageTaskID;
import jdk.jpackage.internal.PackagingPipeline.PrimaryTaskID;
import jdk.jpackage.internal.PackagingPipeline.TaskID;
import jdk.jpackage.internal.model.ConfigException;
import jdk.jpackage.internal.model.LinuxPackage;
import jdk.jpackage.internal.model.PackagerException;
abstract class LinuxPackager<T extends LinuxPackage> implements Consumer<PackagingPipeline.Builder> {
LinuxPackager(BuildEnv env, T pkg, Path outputDir, LinuxSystemEnvironment sysEnv) {
this.env = Objects.requireNonNull(env);
this.pkg = Objects.requireNonNull(pkg);
this.outputDir = Objects.requireNonNull(outputDir);
this.withRequiredPackagesLookup = sysEnv.soLookupAvailable() && sysEnv.nativePackageType().equals(pkg.type());
customActions = List.of(
DesktopIntegration.create(env, pkg),
LinuxLaunchersAsServices.create(env, pkg));
}
enum LinuxPackageTaskID implements TaskID {
INIT_REQUIRED_PACKAGES,
VERIFY_PACKAGE
}
@Override
public void accept(PackagingPipeline.Builder pipelineBuilder) {
pipelineBuilder.excludeDirFromCopying(outputDir)
.task(PackageTaskID.CREATE_CONFIG_FILES)
.action(this::buildConfigFiles)
.add()
.task(LinuxPackageTaskID.INIT_REQUIRED_PACKAGES)
.addDependencies(PrimaryTaskID.BUILD_APPLICATION_IMAGE, PrimaryTaskID.COPY_APP_IMAGE)
.addDependent(PackageTaskID.CREATE_CONFIG_FILES)
.action(this::initRequiredPackages)
.add()
.task(LinuxPackageTaskID.VERIFY_PACKAGE)
.addDependencies(PackageTaskID.CREATE_PACKAGE_FILE)
.addDependent(PrimaryTaskID.PACKAGE)
.action(this::verifyOutputPackage)
.add()
.task(PackageTaskID.CREATE_PACKAGE_FILE)
.action(this::buildPackage)
.add();
}
protected final Path outputPackageFile() {
return outputDir.resolve(pkg.packageFileNameWithSuffix());
}
protected abstract void buildPackage() throws IOException;
protected abstract List<? extends Exception> findErrorsInOutputPackage() throws IOException;
protected abstract void createConfigFiles(Map<String, String> replacementData) throws IOException;
protected abstract Map<String, String> createReplacementData() throws IOException;
protected abstract void initLibProvidersLookup(LibProvidersLookup libProvidersLookup);
private void buildConfigFiles() throws PackagerException, IOException {
final var data = createDefaultReplacementData();
for (var ca : customActions) {
ShellCustomAction.mergeReplacementData(data, ca.create());
}
data.putAll(createReplacementData());
createConfigFiles(Collections.unmodifiableMap(data));
}
private Map<String, String> createDefaultReplacementData() {
Map<String, String> data = new HashMap<>();
data.put("APPLICATION_PACKAGE", pkg.packageName());
data.put("APPLICATION_VENDOR", pkg.app().vendor());
data.put("APPLICATION_VERSION", pkg.version());
data.put("APPLICATION_DESCRIPTION", pkg.description());
String defaultDeps = String.join(", ", requiredPackages);
String customDeps = pkg.additionalDependencies().orElse("");
if (!customDeps.isEmpty() && !defaultDeps.isEmpty()) {
customDeps = ", " + customDeps;
}
data.put("PACKAGE_DEFAULT_DEPENDENCIES", defaultDeps);
data.put("PACKAGE_CUSTOM_DEPENDENCIES", customDeps);
return data;
}
private void initRequiredPackages() throws IOException {
final List<String> caPackages = customActions.stream()
.map(ShellCustomAction::requiredPackages)
.flatMap(List::stream).toList();
final List<String> neededLibPackages;
if (withRequiredPackagesLookup) {
neededLibPackages = findRequiredPackages();
} else {
neededLibPackages = Collections.emptyList();
Log.info(I18N.getString("warning.foreign-app-image"));
}
// Merge all package lists together.
// Filter out empty names, sort and remove duplicates.
Stream.of(caPackages, neededLibPackages)
.flatMap(List::stream)
.filter(Predicate.not(String::isEmpty))
.sorted().distinct().forEach(requiredPackages::add);
Log.verbose(String.format("Required packages: %s", requiredPackages));
}
private List<String> findRequiredPackages() throws IOException {
var lookup = new LibProvidersLookup();
initLibProvidersLookup(lookup);
return lookup.execute(env.appImageDir());
}
private void verifyOutputPackage() {
final List<? extends Exception> errors;
try {
errors = findErrorsInOutputPackage();
} catch (Exception ex) {
// Ignore error as it is not critical. Just report it.
Log.verbose(ex);
return;
}
for (var ex : errors) {
Log.verbose(ex.getLocalizedMessage());
if (ex instanceof ConfigException cfgEx) {
Log.verbose(cfgEx.getAdvice());
}
}
}
protected final BuildEnv env;
protected final T pkg;
protected final Path outputDir;
private final boolean withRequiredPackagesLookup;
private final List<String> requiredPackages = new ArrayList<>();
private final List<ShellCustomAction> customActions;
}

View File

@ -25,189 +25,20 @@
package jdk.jpackage.internal;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import jdk.internal.util.OperatingSystem;
import jdk.jpackage.internal.model.ConfigException;
import jdk.jpackage.internal.model.DottedVersion;
import jdk.jpackage.internal.model.LinuxPackage;
import jdk.jpackage.internal.model.LinuxRpmPackage;
import jdk.jpackage.internal.model.Package;
import jdk.jpackage.internal.model.PackagerException;
import jdk.jpackage.internal.model.StandardPackageType;
import jdk.jpackage.internal.util.Result;
/**
* There are two command line options to configure license information for RPM
* packaging: --linux-rpm-license-type and --license-file. Value of
* --linux-rpm-license-type command line option configures "License:" section
* of RPM spec. Value of --license-file command line option specifies a license
* file to be added to the package. License file is a sort of documentation file
* but it will be installed even if user selects an option to install the
* package without documentation. --linux-rpm-license-type is the primary option
* to set license information. --license-file makes little sense in case of RPM
* packaging.
*/
public class LinuxRpmBundler extends LinuxPackageBundler {
private static final String DEFAULT_SPEC_TEMPLATE = "template.spec";
public static final String TOOL_RPM = "rpm";
public static final String TOOL_RPMBUILD = "rpmbuild";
public static final DottedVersion TOOL_RPMBUILD_MIN_VERSION = DottedVersion.lazy(
"4.10");
public LinuxRpmBundler() {
super(LinuxFromParams.RPM_PACKAGE);
}
@Override
protected void doValidate(BuildEnv env, LinuxPackage pkg) throws ConfigException {
}
private static ToolValidator createRpmbuildToolValidator() {
Pattern pattern = Pattern.compile(" (\\d+\\.\\d+)");
return new ToolValidator(TOOL_RPMBUILD).setMinimalVersion(
TOOL_RPMBUILD_MIN_VERSION).setVersionParser(lines -> {
String versionString = lines.limit(1).collect(
Collectors.toList()).get(0);
Matcher matcher = pattern.matcher(versionString);
if (matcher.find()) {
return matcher.group(1);
}
return null;
});
}
@Override
protected List<ToolValidator> getToolValidators() {
return List.of(createRpmbuildToolValidator());
}
protected void createConfigFiles(Map<String, String> replacementData,
BuildEnv env, LinuxPackage pkg) throws IOException {
Path specFile = specFile(env, pkg);
// prepare spec file
env.createResource(DEFAULT_SPEC_TEMPLATE)
.setCategory(I18N.getString("resource.rpm-spec-file"))
.setSubstitutionData(replacementData)
.saveToFile(specFile);
}
@Override
protected Path buildPackageBundle(BuildEnv env, LinuxPackage pkg,
Path outputParentDir) throws PackagerException, IOException {
return buildRPM(env, pkg, outputParentDir);
}
private static Path installPrefix(LinuxPackage pkg) {
Path path = pkg.relativeInstallDir();
if (!pkg.isInstallDirInUsrTree()) {
path = path.getParent();
}
return Path.of("/").resolve(path);
}
@Override
protected Map<String, String> createReplacementData(BuildEnv env, LinuxPackage pkg) throws IOException {
Map<String, String> data = new HashMap<>();
data.put("APPLICATION_RELEASE", pkg.release().orElseThrow());
data.put("APPLICATION_PREFIX", installPrefix(pkg).toString());
data.put("APPLICATION_DIRECTORY", Path.of("/").resolve(pkg.relativeInstallDir()).toString());
data.put("APPLICATION_SUMMARY", pkg.app().name());
data.put("APPLICATION_LICENSE_TYPE", ((LinuxRpmPackage)pkg).licenseType());
String licenseFile = pkg.licenseFile().map(v -> {
return v.toAbsolutePath().normalize().toString();
}).orElse(null);
data.put("APPLICATION_LICENSE_FILE", licenseFile);
data.put("APPLICATION_GROUP", pkg.category().orElse(""));
data.put("APPLICATION_URL", pkg.aboutURL().orElse(""));
return data;
}
@Override
protected void initLibProvidersLookup(LibProvidersLookup libProvidersLookup) {
libProvidersLookup.setPackageLookup(file -> {
return Executor.of(TOOL_RPM,
"-q", "--queryformat", "%{name}\\n",
"-q", "--whatprovides", file.toString())
.saveOutput(true).executeExpectSuccess().getOutput().stream();
});
}
@Override
protected List<ConfigException> verifyOutputBundle(BuildEnv env, LinuxPackage pkg,
Path packageBundle) {
List<ConfigException> errors = new ArrayList<>();
String specFileName = specFile(env, pkg).getFileName().toString();
try {
List<PackageProperty> properties = List.of(
new PackageProperty("Name", pkg.packageName(),
"APPLICATION_PACKAGE", specFileName),
new PackageProperty("Version", pkg.version(),
"APPLICATION_VERSION", specFileName),
new PackageProperty("Release", pkg.release().orElseThrow(),
"APPLICATION_RELEASE", specFileName),
new PackageProperty("Arch", pkg.arch(), null, specFileName));
List<String> actualValues = Executor.of(TOOL_RPM, "-qp", "--queryformat",
properties.stream().map(entry -> String.format("%%{%s}",
entry.name)).collect(Collectors.joining("\\n")),
packageBundle.toString()).saveOutput(true).executeExpectSuccess().getOutput();
Iterator<String> actualValuesIt = actualValues.iterator();
properties.forEach(property -> errors.add(property.verifyValue(
actualValuesIt.next())));
} catch (IOException ex) {
// Ignore error as it is not critical. Just report it.
Log.verbose(ex);
}
return errors;
}
private Path specFile(BuildEnv env, Package pkg) {
return env.buildRoot().resolve(Path.of("SPECS", pkg.packageName() + ".spec"));
}
private Path buildRPM(BuildEnv env, Package pkg, Path outdir) throws IOException {
Path rpmFile = outdir.toAbsolutePath().resolve(pkg.packageFileNameWithSuffix());
Log.verbose(I18N.format("message.outputting-bundle-location", rpmFile.getParent()));
//run rpmbuild
Executor.of(TOOL_RPMBUILD,
"-bb", specFile(env, pkg).toAbsolutePath().toString(),
"--define", String.format("%%_sourcedir %s",
env.appImageDir().toAbsolutePath()),
// save result to output dir
"--define", String.format("%%_rpmdir %s", rpmFile.getParent()),
// do not use other system directories to build as current user
"--define", String.format("%%_topdir %s",
env.buildRoot().toAbsolutePath()),
"--define", String.format("%%_rpmfilename %s", rpmFile.getFileName())
).executeExpectSuccess();
Log.verbose(I18N.format("message.output-bundle-location", rpmFile.getParent()));
return rpmFile;
}
@Override
public String getName() {
return I18N.getString("rpm.bundler.name");
@ -219,12 +50,28 @@ public class LinuxRpmBundler extends LinuxPackageBundler {
}
@Override
public boolean supported(boolean runtimeInstaller) {
return OperatingSystem.isLinux() && (createRpmbuildToolValidator().validate() == null);
public Path execute(Map<String, ? super Object> params, Path outputParentDir) throws PackagerException {
return Packager.<LinuxRpmPackage>build().outputDir(outputParentDir)
.pkg(LinuxFromParams.RPM_PACKAGE.fetchFrom(params))
.env(BuildEnvFromParams.BUILD_ENV.fetchFrom(params))
.pipelineBuilderMutatorFactory((env, pkg, outputDir) -> {
return new LinuxRpmPackager(env, pkg, outputDir, sysEnv.orElseThrow());
}).execute(LinuxPackagingPipeline.build());
}
@Override
protected Result<LinuxRpmSystemEnvironment> sysEnv() {
return sysEnv;
}
@Override
public boolean isDefault() {
return !LinuxDebBundler.isDebian();
return sysEnv.value()
.map(LinuxSystemEnvironment::nativePackageType)
.map(StandardPackageType.LINUX_RPM::equals)
.orElse(false);
}
private final Result<LinuxRpmSystemEnvironment> sysEnv = LinuxRpmSystemEnvironment.create(SYS_ENV);
}

View File

@ -0,0 +1,167 @@
/*
* 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. 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 static java.util.stream.Collectors.joining;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import jdk.jpackage.internal.model.ConfigException;
import jdk.jpackage.internal.model.LinuxRpmPackage;
/**
* There are two command line options to configure license information for RPM
* packaging: --linux-rpm-license-type and --license-file. Value of
* --linux-rpm-license-type command line option configures "License:" section
* of RPM spec. Value of --license-file command line option specifies a license
* file to be added to the package. License file is a sort of documentation file
* but it will be installed even if user selects an option to install the
* package without documentation. --linux-rpm-license-type is the primary option
* to set license information. --license-file makes little sense in case of RPM
* packaging.
*/
final class LinuxRpmPackager extends LinuxPackager<LinuxRpmPackage> {
LinuxRpmPackager(BuildEnv env, LinuxRpmPackage pkg, Path outputDir, LinuxRpmSystemEnvironment sysEnv) {
super(env, pkg, outputDir, sysEnv);
this.sysEnv = Objects.requireNonNull(sysEnv);
}
@Override
protected void createConfigFiles(Map<String, String> replacementData) throws IOException {
Path specFile = specFile();
// prepare spec file
env.createResource("template.spec")
.setCategory(I18N.getString("resource.rpm-spec-file"))
.setSubstitutionData(replacementData)
.saveToFile(specFile);
}
@Override
protected Map<String, String> createReplacementData() {
Map<String, String> data = new HashMap<>();
data.put("APPLICATION_RELEASE", pkg.release().orElseThrow());
data.put("APPLICATION_PREFIX", installPrefix().toString());
data.put("APPLICATION_DIRECTORY", Path.of("/").resolve(pkg.relativeInstallDir()).toString());
data.put("APPLICATION_SUMMARY", pkg.app().name());
data.put("APPLICATION_LICENSE_TYPE", pkg.licenseType());
String licenseFile = pkg.licenseFile().map(v -> {
return v.toAbsolutePath().normalize().toString();
}).orElse(null);
data.put("APPLICATION_LICENSE_FILE", licenseFile);
data.put("APPLICATION_GROUP", pkg.category().orElse(""));
data.put("APPLICATION_URL", pkg.aboutURL().orElse(""));
return data;
}
@Override
protected void initLibProvidersLookup(LibProvidersLookup libProvidersLookup) {
libProvidersLookup.setPackageLookup(file -> {
return Executor.of(sysEnv.rpm().toString(),
"-q", "--queryformat", "%{name}\\n",
"-q", "--whatprovides", file.toString()
).saveOutput(true).executeExpectSuccess().getOutput().stream();
});
}
@Override
protected List<? extends Exception> findErrorsInOutputPackage() throws IOException {
List<ConfigException> errors = new ArrayList<>();
var specFileName = specFile().getFileName().toString();
var properties = List.of(
new PackageProperty("Name", pkg.packageName(),
"APPLICATION_PACKAGE", specFileName),
new PackageProperty("Version", pkg.version(),
"APPLICATION_VERSION", specFileName),
new PackageProperty("Release", pkg.release().orElseThrow(),
"APPLICATION_RELEASE", specFileName),
new PackageProperty("Arch", pkg.arch(), null, specFileName));
var actualValues = Executor.of(
sysEnv.rpm().toString(),
"-qp",
"--queryformat", properties.stream().map(e -> String.format("%%{%s}", e.name)).collect(joining("\\n")),
outputPackageFile().toString()
).saveOutput(true).executeExpectSuccess().getOutput();
for (int i = 0; i != properties.size(); i++) {
Optional.ofNullable(properties.get(i).verifyValue(actualValues.get(i))).ifPresent(errors::add);
}
return errors;
}
@Override
protected void buildPackage() throws IOException {
Path rpmFile = outputPackageFile();
Log.verbose(I18N.format("message.outputting-bundle-location", rpmFile.getParent()));
//run rpmbuild
Executor.of(sysEnv.rpmbuild().toString(),
"-bb", specFile().toAbsolutePath().toString(),
"--define", String.format("%%_sourcedir %s",
env.appImageDir().toAbsolutePath()),
// save result to output dir
"--define", String.format("%%_rpmdir %s", rpmFile.getParent()),
// do not use other system directories to build as current user
"--define", String.format("%%_topdir %s",
env.buildRoot().toAbsolutePath()),
"--define", String.format("%%_rpmfilename %s", rpmFile.getFileName())
).executeExpectSuccess();
Log.verbose(I18N.format("message.output-bundle-location", rpmFile.getParent()));
}
private Path installPrefix() {
Path path = pkg.relativeInstallDir();
if (!pkg.isInstallDirInUsrTree()) {
path = path.getParent();
}
return Path.of("/").resolve(path);
}
private Path specFile() {
return env.buildRoot().resolve(Path.of("SPECS", pkg.packageName() + ".spec"));
}
private final LinuxRpmSystemEnvironment sysEnv;
}

View File

@ -0,0 +1,36 @@
/*
* 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. 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 static jdk.jpackage.internal.LinuxSystemEnvironment.mixin;
import jdk.jpackage.internal.util.Result;
public interface LinuxRpmSystemEnvironment extends LinuxSystemEnvironment, LinuxRpmSystemEnvironmentMixin {
static Result<LinuxRpmSystemEnvironment> create(Result<LinuxSystemEnvironment> base) {
return mixin(LinuxRpmSystemEnvironment.class, base, LinuxRpmSystemEnvironmentMixin::create);
}
}

View File

@ -0,0 +1,74 @@
/*
* 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. 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.nio.file.Path;
import java.util.Objects;
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.Result;
public interface LinuxRpmSystemEnvironmentMixin {
Path rpm();
Path rpmbuild();
record Stub(Path rpm, Path rpmbuild) implements LinuxRpmSystemEnvironmentMixin {
}
static Result<LinuxRpmSystemEnvironmentMixin> create() {
final var errors = Stream.of(
Internal.createRpmbuildToolValidator(),
new ToolValidator(Internal.TOOL_RPM)
).map(ToolValidator::validate).filter(Objects::nonNull).toList();
if (errors.isEmpty()) {
return Result.ofValue(new Stub(Internal.TOOL_RPM, Internal.TOOL_RPMBUILD));
} else {
return Result.ofErrors(errors);
}
}
static final class Internal {
private static ToolValidator createRpmbuildToolValidator() {
Pattern pattern = Pattern.compile(" (\\d+\\.\\d+)");
return new ToolValidator(TOOL_RPMBUILD).setMinimalVersion(
TOOL_RPMBUILD_MIN_VERSION).setVersionParser(lines -> {
String versionString = lines.limit(1).findFirst().orElseThrow();
Matcher matcher = pattern.matcher(versionString);
if (matcher.find()) {
return matcher.group(1);
}
return null;
});
}
private static final Path TOOL_RPM = Path.of("rpm");
private static final Path TOOL_RPMBUILD = Path.of("rpmbuild");
private static final DottedVersion TOOL_RPMBUILD_MIN_VERSION = DottedVersion.lazy("4.10");
}
}

View File

@ -0,0 +1,111 @@
/*
* 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. 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.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import jdk.jpackage.internal.model.PackageType;
import jdk.jpackage.internal.model.StandardPackageType;
import jdk.jpackage.internal.util.CompositeProxy;
import jdk.jpackage.internal.util.Result;
public interface LinuxSystemEnvironment extends SystemEnvironment {
boolean soLookupAvailable();
PackageType nativePackageType();
static Result<LinuxSystemEnvironment> create() {
return detectNativePackageType().map(LinuxSystemEnvironment::create).orElseGet(() -> {
return Result.ofError(new RuntimeException("Unknown native package type"));
});
}
static Optional<PackageType> detectNativePackageType() {
if (Internal.isDebian()) {
return Optional.of(StandardPackageType.LINUX_DEB);
} else if (Internal.isRpm()) {
return Optional.of(StandardPackageType.LINUX_RPM);
} else {
return Optional.empty();
}
}
static Result<LinuxSystemEnvironment> create(PackageType nativePackageType) {
return Result.ofValue(new Stub(LibProvidersLookup.supported(),
Objects.requireNonNull(nativePackageType)));
}
static <T, U extends LinuxSystemEnvironment> U createWithMixin(Class<U> type, LinuxSystemEnvironment base, T mixin) {
return CompositeProxy.create(type, base, mixin);
}
static <T, U extends LinuxSystemEnvironment> Result<U> mixin(Class<U> type,
Result<LinuxSystemEnvironment> base, Supplier<Result<T>> mixinResultSupplier) {
final var mixin = mixinResultSupplier.get();
final List<Exception> errors = new ArrayList<>();
errors.addAll(base.errors());
errors.addAll(mixin.errors());
if (errors.isEmpty()) {
return Result.ofValue(createWithMixin(type, base.orElseThrow(), mixin.orElseThrow()));
} else {
return Result.ofErrors(errors);
}
}
record Stub(boolean soLookupAvailable, PackageType nativePackageType) implements LinuxSystemEnvironment {
}
static final class Internal {
private static boolean isDebian() {
// we are just going to run "dpkg -s coreutils" and assume Debian
// or derivative if no error is returned.
try {
Executor.of("dpkg", "-s", "coreutils").executeExpectSuccess();
return true;
} catch (IOException e) {
// just fall thru
return false;
}
}
private static boolean isRpm() {
// we are just going to run "rpm -q rpm" and assume RPM
// or derivative if no error is returned.
try {
Executor.of("rpm", "-q", "rpm").executeExpectSuccess();
return true;
} catch (IOException e) {
// just fall thru
return false;
}
}
}
}

View File

@ -25,12 +25,14 @@
package jdk.jpackage.internal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import jdk.jpackage.internal.model.ConfigException;
import jdk.jpackage.internal.model.MacDmgPackage;
import jdk.jpackage.internal.model.PackagerException;
import jdk.jpackage.internal.util.Result;
public class MacDmgBundler extends MacBaseInstallerBundler {
@ -70,39 +72,27 @@ public class MacDmgBundler extends MacBaseInstallerBundler {
public Path execute(Map<String, ? super Object> params,
Path outputParentDir) throws PackagerException {
final var pkg = MacFromParams.DMG_PACKAGE.fetchFrom(params);
var env = MacBuildEnvFromParams.BUILD_ENV.fetchFrom(params);
var pkg = MacFromParams.DMG_PACKAGE.fetchFrom(params);
final var packager = MacDmgPackager.build().outputDir(outputParentDir).pkg(pkg).env(env);
Log.verbose(I18N.format("message.building-dmg", pkg.app().name()));
MacDmgPackager.findSetFileUtility().ifPresent(packager::setFileUtility);
return packager.execute();
return Packager.<MacDmgPackage>build().outputDir(outputParentDir)
.pkg(pkg)
.env(MacBuildEnvFromParams.BUILD_ENV.fetchFrom(params))
.pipelineBuilderMutatorFactory((env, _, outputDir) -> {
return new MacDmgPackager(env, pkg, outputDir, sysEnv.orElseThrow());
}).execute(MacPackagingPipeline.build(Optional.of(pkg)));
}
@Override
public boolean supported(boolean runtimeInstaller) {
return isSupported();
}
public static final String[] required =
{"/usr/bin/hdiutil", "/usr/bin/osascript"};
public static boolean isSupported() {
try {
for (String s : required) {
Path f = Path.of(s);
if (!Files.exists(f) || !Files.isExecutable(f)) {
return false;
}
}
return true;
} catch (Exception e) {
return false;
}
return sysEnv.hasValue();
}
@Override
public boolean isDefault() {
return true;
}
private final Result<MacDmgSystemEnvironment> sysEnv = MacDmgSystemEnvironment.create();
}

View File

@ -36,106 +36,25 @@ import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import java.util.function.Consumer;
import jdk.jpackage.internal.PackagingPipeline.PackageTaskID;
import jdk.jpackage.internal.PackagingPipeline.StartupParameters;
import jdk.jpackage.internal.PackagingPipeline.TaskID;
import jdk.jpackage.internal.model.MacDmgPackage;
import jdk.jpackage.internal.model.PackagerException;
import jdk.jpackage.internal.util.FileUtils;
import jdk.jpackage.internal.util.PathGroup;
record MacDmgPackager(MacDmgPackage pkg, BuildEnv env, Path hdiutil, Path outputDir, Optional<Path> setFileUtility) {
record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir,
MacDmgSystemEnvironment sysEnv) implements Consumer<PackagingPipeline.Builder> {
MacDmgPackager {
Objects.requireNonNull(pkg);
Objects.requireNonNull(env);
Objects.requireNonNull(hdiutil);
Objects.requireNonNull(pkg);
Objects.requireNonNull(outputDir);
Objects.requireNonNull(setFileUtility);
Objects.requireNonNull(sysEnv);
}
static Builder build() {
return new Builder();
}
static final class Builder extends PackagerBuilder<MacDmgPackage, Builder> {
Builder hdiutil(Path v) {
hdiutil = v;
return this;
}
Builder setFileUtility(Path v) {
setFileUtility = v;
return this;
}
Path execute() throws PackagerException {
Log.verbose(MessageFormat.format(I18N.getString("message.building-dmg"),
pkg.app().name()));
IOUtils.writableOutputDir(outputDir);
return execute(MacPackagingPipeline.build(Optional.of(pkg)));
}
@Override
protected void configurePackagingPipeline(PackagingPipeline.Builder pipelineBuilder,
StartupParameters startupParameters) {
final var packager = new MacDmgPackager(pkg, startupParameters.packagingEnv(),
validatedHdiutil(), outputDir, Optional.ofNullable(setFileUtility));
packager.applyToPipeline(pipelineBuilder);
}
private Path validatedHdiutil() {
return Optional.ofNullable(hdiutil).orElse(HDIUTIL);
}
private Path hdiutil;
private Path setFileUtility;
}
// 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
static Optional<Path> findSetFileUtility() {
String typicalPaths[] = {"/Developer/Tools/SetFile",
"/usr/bin/SetFile", "/Developer/usr/bin/SetFile"};
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 (Files.exists(f) && Files.isExecutable(f)) {
return Optional.of(f.toAbsolutePath());
}
}
} catch (IOException ignored) {}
return Optional.empty();
}
private void applyToPipeline(PackagingPipeline.Builder pipelineBuilder) {
@Override
public void accept(PackagingPipeline.Builder pipelineBuilder) {
pipelineBuilder
.excludeDirFromCopying(outputDir)
.task(DmgPackageTaskID.COPY_DMG_CONTENT)
@ -318,7 +237,7 @@ record MacDmgPackager(MacDmgPackage pkg, BuildEnv env, Path hdiutil, Path output
// create temp image
ProcessBuilder pb = new ProcessBuilder(
hdiutil.toString(),
sysEnv.hdiutil().toString(),
"create",
hdiUtilVerbosityFlag,
"-srcfolder", normalizedAbsolutePathString(srcFolder),
@ -341,7 +260,7 @@ record MacDmgPackager(MacDmgPackage pkg, BuildEnv env, Path hdiutil, Path output
// 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(
hdiutil.toString(),
sysEnv.hdiutil().toString(),
"create",
hdiUtilVerbosityFlag,
"-size", String.valueOf(size),
@ -357,7 +276,7 @@ record MacDmgPackager(MacDmgPackage pkg, BuildEnv env, Path hdiutil, Path output
// mount temp image
pb = new ProcessBuilder(
hdiutil.toString(),
sysEnv.hdiutil().toString(),
"attach",
normalizedAbsolutePathString(protoDMG),
hdiUtilVerbosityFlag,
@ -382,7 +301,7 @@ record MacDmgPackager(MacDmgPackage pkg, BuildEnv env, Path hdiutil, Path output
// to install-dir in DMG as critical error, since it can fail in
// headless environment.
try {
pb = new ProcessBuilder("/usr/bin/osascript",
pb = new ProcessBuilder(sysEnv.osascript().toString(),
normalizedAbsolutePathString(volumeScript()));
IOUtils.exec(pb, 180); // Wait 3 minutes. See JDK-8248248.
} catch (IOException ex) {
@ -397,7 +316,7 @@ record MacDmgPackager(MacDmgPackage pkg, BuildEnv env, Path hdiutil, Path output
// NB: attributes of the root directory are ignored
// when creating the volume
// Therefore we have to do this after we mount image
if (setFileUtility.isPresent()) {
if (sysEnv.setFileUtility().isPresent()) {
//can not find utility => keep going without icon
try {
volumeIconFile.toFile().setWritable(true);
@ -406,14 +325,14 @@ record MacDmgPackager(MacDmgPackage pkg, BuildEnv env, Path hdiutil, Path output
// "icnC" for the volume icon
// (might not work on Mac 10.13 with old XCode)
pb = new ProcessBuilder(
setFileUtility.orElseThrow().toString(),
sysEnv.setFileUtility().orElseThrow().toString(),
"-c", "icnC",
normalizedAbsolutePathString(volumeIconFile));
IOUtils.exec(pb);
volumeIconFile.toFile().setReadOnly();
pb = new ProcessBuilder(
setFileUtility.orElseThrow().toString(),
sysEnv.setFileUtility().orElseThrow().toString(),
"-a", "C",
normalizedAbsolutePathString(mountedVolume));
IOUtils.exec(pb);
@ -428,7 +347,7 @@ record MacDmgPackager(MacDmgPackage pkg, BuildEnv env, Path hdiutil, Path output
} finally {
// Detach the temporary image
pb = new ProcessBuilder(
hdiutil.toString(),
sysEnv.hdiutil().toString(),
"detach",
hdiUtilVerbosityFlag,
normalizedAbsolutePathString(mountedVolume));
@ -451,7 +370,7 @@ record MacDmgPackager(MacDmgPackage pkg, BuildEnv env, Path hdiutil, Path output
// Now force to detach if it still attached
if (Files.exists(mountedVolume)) {
pb = new ProcessBuilder(
hdiutil.toString(),
sysEnv.hdiutil().toString(),
"detach",
"-force",
hdiUtilVerbosityFlag,
@ -464,7 +383,7 @@ record MacDmgPackager(MacDmgPackage pkg, BuildEnv env, Path hdiutil, Path output
// Compress it to a new image
pb = new ProcessBuilder(
hdiutil.toString(),
sysEnv.hdiutil().toString(),
"convert",
normalizedAbsolutePathString(protoDMG),
hdiUtilVerbosityFlag,
@ -481,7 +400,7 @@ record MacDmgPackager(MacDmgPackage pkg, BuildEnv env, Path hdiutil, Path output
Files.copy(protoDMG, protoCopyDMG);
try {
pb = new ProcessBuilder(
hdiutil.toString(),
sysEnv.hdiutil().toString(),
"convert",
normalizedAbsolutePathString(protoCopyDMG),
hdiUtilVerbosityFlag,
@ -496,7 +415,7 @@ record MacDmgPackager(MacDmgPackage pkg, BuildEnv env, Path hdiutil, Path output
//add license if needed
if (pkg.licenseFile().isPresent()) {
pb = new ProcessBuilder(
hdiutil.toString(),
sysEnv.hdiutil().toString(),
"udifrez",
normalizedAbsolutePathString(finalDMG),
"-xml",
@ -527,6 +446,4 @@ record MacDmgPackager(MacDmgPackage pkg, BuildEnv env, Path hdiutil, Path output
private static final String TEMPLATE_BUNDLE_ICON = "JavaApp.icns";
private static final String DEFAULT_LICENSE_PLIST="lic_template.plist";
private static final Path HDIUTIL = Path.of("/usr/bin/hdiutil");
}

View File

@ -0,0 +1,94 @@
/*
* 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. 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.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import jdk.jpackage.internal.util.Result;
record MacDmgSystemEnvironment(Path hdiutil, Path osascript, Optional<Path> setFileUtility) implements SystemEnvironment {
MacDmgSystemEnvironment {
}
static Result<MacDmgSystemEnvironment> create() {
final var errors = Stream.of(HDIUTIL, OSASCRIPT)
.map(ToolValidator::new)
.map(ToolValidator::checkExistsOnly)
.map(ToolValidator::validate)
.filter(Objects::nonNull)
.toList();
if (errors.isEmpty()) {
return Result.ofValue(new MacDmgSystemEnvironment(HDIUTIL, OSASCRIPT, findSetFileUtility()));
} else {
return Result.ofErrors(errors);
}
}
// 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"};
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();
}
private static final Path HDIUTIL = Path.of("/usr/bin/hdiutil");
private static final Path OSASCRIPT = Path.of("/usr/bin/osascript");
}

View File

@ -28,7 +28,9 @@ package jdk.jpackage.internal;
import java.nio.file.Path;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import jdk.jpackage.internal.model.ConfigException;
import jdk.jpackage.internal.model.MacPkgPackage;
import jdk.jpackage.internal.model.PackagerException;
public class MacPkgBundler extends MacBaseInstallerBundler {
@ -49,7 +51,7 @@ public class MacPkgBundler extends MacBaseInstallerBundler {
try {
Objects.requireNonNull(params);
final var pkgPkg = MacFromParams.PKG_PACKAGE.fetchFrom(params);
final var pkg = MacFromParams.PKG_PACKAGE.fetchFrom(params);
// run basic validation to ensure requirements are met
// we are not interested in return code, only possible exception
@ -72,12 +74,16 @@ public class MacPkgBundler extends MacBaseInstallerBundler {
public Path execute(Map<String, ? super Object> params,
Path outputParentDir) throws PackagerException {
final var pkg = MacFromParams.PKG_PACKAGE.fetchFrom(params);
var env = MacBuildEnvFromParams.BUILD_ENV.fetchFrom(params);
var pkg = MacFromParams.PKG_PACKAGE.fetchFrom(params);
final var packager = MacPkgPackager.build().outputDir(outputParentDir).pkg(pkg).env(env);
Log.verbose(I18N.format("message.building-pkg", pkg.app().name()));
return packager.execute();
return Packager.<MacPkgPackage>build().outputDir(outputParentDir)
.pkg(pkg)
.env(MacBuildEnvFromParams.BUILD_ENV.fetchFrom(params))
.pipelineBuilderMutatorFactory((env, _, outputDir) -> {
return new MacPkgPackager(env, pkg, outputDir);
}).execute(MacPackagingPipeline.build(Optional.of(pkg)));
}
@Override

View File

@ -41,6 +41,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Stream;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
@ -52,15 +53,25 @@ import javax.xml.transform.stream.StreamSource;
import jdk.internal.util.Architecture;
import jdk.internal.util.OSVersion;
import jdk.jpackage.internal.PackagingPipeline.PackageTaskID;
import jdk.jpackage.internal.PackagingPipeline.StartupParameters;
import jdk.jpackage.internal.PackagingPipeline.TaskID;
import jdk.jpackage.internal.model.MacPkgPackage;
import jdk.jpackage.internal.model.PackagerException;
import jdk.jpackage.internal.resources.ResourceLocator;
import jdk.jpackage.internal.util.XmlUtils;
import org.xml.sax.SAXException;
record MacPkgPackager(MacPkgPackage pkg, BuildEnv env, Optional<Services> services, Path outputDir) {
record MacPkgPackager(BuildEnv env, MacPkgPackage pkg, Optional<Services> services,
Path outputDir) implements Consumer<PackagingPipeline.Builder> {
MacPkgPackager {
Objects.requireNonNull(env);
Objects.requireNonNull(pkg);
Objects.requireNonNull(services);
Objects.requireNonNull(outputDir);
}
MacPkgPackager(BuildEnv env, MacPkgPackage pkg, Path outputDir) {
this(env, pkg, createServices(env, pkg), outputDir);
}
enum PkgPackageTaskID implements TaskID {
PREPARE_MAIN_SCRIPTS,
@ -69,37 +80,6 @@ record MacPkgPackager(MacPkgPackage pkg, BuildEnv env, Optional<Services> servic
PREPARE_SERVICES
}
static Builder build() {
return new Builder();
}
static final class Builder extends PackagerBuilder<MacPkgPackage, Builder> {
Path execute() throws PackagerException {
Log.verbose(MessageFormat.format(I18N.getString("message.building-pkg"),
pkg.app().name()));
IOUtils.writableOutputDir(outputDir);
return execute(MacPackagingPipeline.build(Optional.of(pkg)));
}
@Override
protected void configurePackagingPipeline(PackagingPipeline.Builder pipelineBuilder,
StartupParameters startupParameters) {
final var packager = new MacPkgPackager(pkg, startupParameters.packagingEnv(), createServices(), outputDir);
packager.applyToPipeline(pipelineBuilder);
}
private Optional<Services> createServices() {
if (pkg.app().isService()) {
return Optional.of(Services.create(pkg, env));
} else {
return Optional.empty();
}
}
}
record InternalPackage(Path srcRoot, String identifier, Path path, List<String> otherPkgbuildArgs) {
InternalPackage {
@ -230,7 +210,8 @@ record MacPkgPackager(MacPkgPackage pkg, BuildEnv env, Optional<Services> servic
private final Optional<String> nameSuffix;
}
private void applyToPipeline(PackagingPipeline.Builder pipelineBuilder) {
@Override
public void accept(PackagingPipeline.Builder pipelineBuilder) {
pipelineBuilder
.excludeDirFromCopying(outputDir)
.task(PkgPackageTaskID.PREPARE_MAIN_SCRIPTS)
@ -559,6 +540,14 @@ record MacPkgPackager(MacPkgPackage pkg, BuildEnv env, Optional<Services> servic
IOUtils.exec(pb, false, null, true, Executor.INFINITE_TIMEOUT);
}
private static Optional<Services> createServices(BuildEnv env, MacPkgPackage pkg) {
if (pkg.app().isService()) {
return Optional.of(Services.create(pkg, env));
} else {
return Optional.empty();
}
}
private static final String DEFAULT_BACKGROUND_IMAGE = "background_pkg.png";
private static final String DEFAULT_PDF = "product-def.plist";
}

View File

@ -26,50 +26,80 @@ package jdk.jpackage.internal;
import java.nio.file.Path;
import java.util.Objects;
import jdk.jpackage.internal.PackagingPipeline.StartupParameters;
import java.util.Optional;
import java.util.function.Consumer;
import jdk.jpackage.internal.model.Package;
import jdk.jpackage.internal.model.PackagerException;
abstract class PackagerBuilder<T extends Package, U extends PackagerBuilder<T, U>> {
final class Packager<T extends Package> {
U pkg(T v) {
static <T extends Package> Packager<T> build() {
return new Packager<>();
}
Packager<T> pkg(T v) {
pkg = v;
return thiz();
return this;
}
U env(BuildEnv v) {
Packager<T> env(BuildEnv v) {
env = v;
return thiz();
return this;
}
U outputDir(Path v) {
Packager<T> outputDir(Path v) {
outputDir = v;
return thiz();
return this;
}
@SuppressWarnings("unchecked")
private U thiz() {
return (U)this;
Packager<T> pipelineBuilderMutatorFactory(PipelineBuilderMutatorFactory<T> v) {
pipelineBuilderMutatorFactory = v;
return this;
}
protected abstract void configurePackagingPipeline(PackagingPipeline.Builder pipelineBuilder,
StartupParameters startupParameters);
T pkg() {
return Objects.requireNonNull(pkg);
}
Path outputDir() {
return Objects.requireNonNull(outputDir);
}
BuildEnv env() {
return Objects.requireNonNull(env);
}
Path execute(PackagingPipeline.Builder pipelineBuilder) throws PackagerException {
Objects.requireNonNull(pkg);
Objects.requireNonNull(env);
Objects.requireNonNull(outputDir);
IOUtils.writableOutputDir(outputDir);
final var startupParameters = pipelineBuilder.createStartupParameters(env, pkg, outputDir);
configurePackagingPipeline(pipelineBuilder, startupParameters);
pipelineBuilderMutatorFactory().ifPresent(factory -> {
factory.create(startupParameters.packagingEnv(), pkg, outputDir).accept(pipelineBuilder);
});
pipelineBuilder.create().execute(startupParameters);
return outputDir.resolve(pkg.packageFileNameWithSuffix());
}
protected T pkg;
protected BuildEnv env;
protected Path outputDir;
@FunctionalInterface
interface PipelineBuilderMutatorFactory<T extends Package> {
Consumer<PackagingPipeline.Builder> create(BuildEnv env, T pkg, Path outputDir);
}
private Optional<PipelineBuilderMutatorFactory<T>> pipelineBuilderMutatorFactory() {
return Optional.ofNullable(pipelineBuilderMutatorFactory);
}
private T pkg;
private BuildEnv env;
private Path outputDir;
private PipelineBuilderMutatorFactory<T> pipelineBuilderMutatorFactory;
}

View File

@ -0,0 +1,28 @@
/*
* 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. 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;
public interface SystemEnvironment {
}

View File

@ -24,36 +24,31 @@
*/
package jdk.jpackage.internal;
import jdk.internal.util.OperatingSystem;
import jdk.jpackage.internal.model.ConfigException;
import jdk.jpackage.internal.model.DottedVersion;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Stream;
import jdk.internal.util.OperatingSystem;
import jdk.jpackage.internal.model.ConfigException;
import jdk.jpackage.internal.model.DottedVersion;
public final class ToolValidator {
final class ToolValidator {
ToolValidator(String tool) {
this(Path.of(tool));
}
ToolValidator(Path toolPath) {
this.toolPath = toolPath;
args = new ArrayList<>();
this.toolPath = Objects.requireNonNull(toolPath);
if (OperatingSystem.isLinux()) {
setCommandLine("--version");
}
setToolNotFoundErrorHandler(null);
setToolOldVersionErrorHandler(null);
}
ToolValidator setCommandLine(String... args) {
@ -67,7 +62,17 @@ public final class ToolValidator {
}
ToolValidator setMinimalVersion(DottedVersion v) {
return setMinimalVersion(t -> DottedVersion.compareComponents(v, DottedVersion.lazy(t)));
return setMinimalVersion(new Comparable<String>() {
@Override
public int compareTo(String o) {
return DottedVersion.compareComponents(v, DottedVersion.lazy(o));
}
@Override
public String toString() {
return v.toString();
}
});
}
ToolValidator setVersionParser(Function<Stream<String>, String> v) {
@ -75,69 +80,85 @@ public final class ToolValidator {
return this;
}
ToolValidator setToolNotFoundErrorHandler(
BiFunction<String, IOException, ConfigException> v) {
ToolValidator setToolNotFoundErrorHandler(Function<Path, ConfigException> v) {
toolNotFoundErrorHandler = v;
return this;
}
ToolValidator setToolOldVersionErrorHandler(BiFunction<String, String, ConfigException> v) {
ToolValidator setToolOldVersionErrorHandler(BiFunction<Path, String, ConfigException> v) {
toolOldVersionErrorHandler = v;
return this;
}
ToolValidator checkExistsOnly(boolean v) {
checkExistsOnly = v;
return this;
}
ToolValidator checkExistsOnly() {
return checkExistsOnly(true);
}
ConfigException validate() {
if (checkExistsOnly) {
if (Files.isExecutable(toolPath) && !Files.isDirectory(toolPath)) {
return null;
} else if (Files.exists(toolPath)) {
return new ConfigException(
I18N.format("error.tool-not-executable", toolPath), (String)null);
} else if (toolNotFoundErrorHandler != null) {
return toolNotFoundErrorHandler.apply(toolPath);
} else {
return new ConfigException(
I18N.format("error.tool-not-found", toolPath),
I18N.format("error.tool-not-found.advice", toolPath));
}
}
List<String> cmdline = new ArrayList<>();
cmdline.add(toolPath.toString());
cmdline.addAll(args);
if (args != null) {
cmdline.addAll(args);
}
boolean canUseTool[] = new boolean[1];
if (minimalVersion == null) {
// No version check.
canUseTool[0] = true;
}
String[] version = new String[1];
String name = IOUtils.getFileName(toolPath).toString();
try {
ProcessBuilder pb = new ProcessBuilder(cmdline);
AtomicBoolean canUseTool = new AtomicBoolean();
if (minimalVersion == null) {
// No version check.
canUseTool.setPlain(true);
}
String[] version = new String[1];
Executor.of(pb).setQuiet(true).setOutputConsumer(lines -> {
Executor.of(cmdline.toArray(String[]::new)).setQuiet(true).setOutputConsumer(lines -> {
if (versionParser != null && minimalVersion != null) {
version[0] = versionParser.apply(lines);
if (minimalVersion.compareTo(version[0]) < 0) {
canUseTool.setPlain(true);
if (version[0] != null && minimalVersion.compareTo(version[0]) <= 0) {
canUseTool[0] = true;
}
}
}).execute();
if (!canUseTool.getPlain()) {
if (toolOldVersionErrorHandler != null) {
return toolOldVersionErrorHandler.apply(name, version[0]);
}
return new ConfigException(MessageFormat.format(I18N.getString(
"error.tool-old-version"), name, minimalVersion),
MessageFormat.format(I18N.getString(
"error.tool-old-version.advice"), name,
minimalVersion));
}
} catch (IOException e) {
if (toolNotFoundErrorHandler != null) {
return toolNotFoundErrorHandler.apply(name, e);
}
return new ConfigException(MessageFormat.format(I18N.getString(
"error.tool-not-found"), name, e.getMessage()),
MessageFormat.format(I18N.getString(
"error.tool-not-found.advice"), name), e);
return new ConfigException(I18N.format("error.tool-error", toolPath, e.getMessage()), null, e);
}
// All good. Tool can be used.
return null;
if (canUseTool[0]) {
// All good. Tool can be used.
return null;
} else if (toolOldVersionErrorHandler != null) {
return toolOldVersionErrorHandler.apply(toolPath, version[0]);
} else {
return new ConfigException(
I18N.format("error.tool-old-version", toolPath, minimalVersion),
I18N.format("error.tool-old-version.advice", toolPath, minimalVersion));
}
}
private final Path toolPath;
private List<String> args;
private Comparable<String> minimalVersion;
private Function<Stream<String>, String> versionParser;
private BiFunction<String, IOException, ConfigException> toolNotFoundErrorHandler;
private BiFunction<String, String, ConfigException> toolOldVersionErrorHandler;
private Function<Path, ConfigException> toolNotFoundErrorHandler;
private BiFunction<Path, String, ConfigException> toolOldVersionErrorHandler;
private boolean checkExistsOnly;
}

View File

@ -66,10 +66,13 @@ error.no-content-types-for-file-association.advice=Specify MIME type for File As
error.too-many-content-types-for-file-association=More than one MIME types was specified for File Association number {0}
error.too-many-content-types-for-file-association.advice=Specify only one MIME type for File Association number {0}
error.tool-not-found=Can not find {0}. Reason: {1}
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.tool-error=Can not validate "{0}". Reason: {1}
error.tool-not-executable="{0}" is not executable
error.tool-not-found=Can not find "{0}"
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

View File

@ -0,0 +1,125 @@
/*
* 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.internal.util;
import static jdk.jpackage.internal.util.function.ExceptionBox.rethrowUnchecked;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import java.util.stream.StreamSupport;
public record Result<T>(Optional<T> value, Collection<? extends Exception> errors) {
public Result {
if (value.isEmpty() == errors.isEmpty()) {
throw new IllegalArgumentException();
}
if (value.isEmpty() && errors.isEmpty()) {
throw new IllegalArgumentException("Error collection must be non-empty");
}
}
public T orElseThrow() {
firstError().ifPresent(ex -> {
rethrowUnchecked(ex);
});
return value.orElseThrow();
}
public boolean hasValue() {
return value.isPresent();
}
public boolean hasErrors() {
return !errors.isEmpty();
}
public <U> Result<U> map(Function<T, U> conv) {
return new Result<>(value.map(conv), errors);
}
public <U> Result<U> flatMap(Function<T, Result<U>> conv) {
return value.map(conv).orElseGet(() -> {
return new Result<>(Optional.empty(), errors);
});
}
public Result<T> mapErrors(UnaryOperator<Collection<? extends Exception>> errorsMapper) {
return new Result<>(value, errorsMapper.apply(errors));
}
public <U> Result<U> mapErrors() {
return new Result<>(Optional.empty(), errors);
}
public Result<T> peekErrors(Consumer<Collection<? extends Exception>> consumer) {
if (hasErrors()) {
consumer.accept(errors);
}
return this;
}
public Result<T> peekValue(Consumer<T> consumer) {
value.ifPresent(consumer);
return this;
}
public Optional<? extends Exception> firstError() {
return errors.stream().findFirst();
}
public static <T> Result<T> create(Supplier<T> supplier) {
try {
return ofValue(supplier.get());
} catch (Exception ex) {
return ofError(ex);
}
}
public static <T> Result<T> ofValue(T value) {
return new Result<>(Optional.of(value), List.of());
}
public static <T> Result<T> ofErrors(Collection<? extends Exception> errors) {
return new Result<>(Optional.empty(), List.copyOf(errors));
}
public static <T> Result<T> ofError(Exception error) {
return ofErrors(List.of(error));
}
public static boolean allHaveValues(Iterable<? extends Result<?>> results) {
return StreamSupport.stream(results.spliterator(), false).allMatch(Result::hasValue);
}
public static boolean allHaveValues(Result<?>... results) {
return allHaveValues(List.of(results));
}
}

View File

@ -24,17 +24,11 @@
*/
package jdk.jpackage.internal;
import static jdk.jpackage.internal.StandardBundlerParam.ICON;
import static jdk.jpackage.internal.util.function.ThrowingRunnable.toRunnable;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Map;
import jdk.jpackage.internal.model.ConfigException;
import jdk.jpackage.internal.model.PackagerException;
import jdk.jpackage.internal.model.WinExePackage;
import jdk.jpackage.internal.model.WinMsiPackage;
@SuppressWarnings("restricted")
public class WinExeBundler extends AbstractBundler {
@ -79,63 +73,23 @@ public class WinExeBundler extends AbstractBundler {
throws PackagerException {
// Order is important!
var pkg = WinFromParams.MSI_PACKAGE.fetchFrom(params);
var pkg = WinFromParams.EXE_PACKAGE.fetchFrom(params);
var env = BuildEnvFromParams.BUILD_ENV.fetchFrom(params);
IOUtils.writableOutputDir(outdir);
var msiOutputDir = env.buildRoot().resolve("msi");
Path msiDir = env.buildRoot().resolve("msi");
toRunnable(() -> Files.createDirectories(msiDir)).run();
// Write msi to temporary directory.
Path msi = msiBundler.execute(params, msiDir);
try {
new ScriptRunner()
.setDirectory(msi.getParent())
.setResourceCategoryId("resource.post-msi-script")
.setScriptNameSuffix("post-msi")
.setEnvironmentVariable("JpMsiFile", msi.toAbsolutePath().toString())
.run(env, pkg.packageName());
var exePkg = new WinExePackageBuilder(pkg).icon(ICON.fetchFrom(params)).create();
return buildEXE(env, exePkg, msi, outdir);
} catch (IOException|ConfigException ex) {
Log.verbose(ex);
throw new PackagerException(ex);
}
return Packager.<WinMsiPackage>build().outputDir(msiOutputDir)
.pkg(pkg.msiPackage())
.env(env)
.pipelineBuilderMutatorFactory((packagingEnv, msiPackage, _) -> {
var msiPackager = new WinMsiPackager(packagingEnv, msiPackage,
msiOutputDir, msiBundler.sysEnv.orElseThrow());
var exePackager = new WinExePackager(packagingEnv, pkg, outdir, msiOutputDir);
return msiPackager.andThen(exePackager);
}).execute(WinPackagingPipeline.build());
}
private Path buildEXE(BuildEnv env, WinExePackage pkg, Path msi,
Path outdir) throws IOException {
Log.verbose(I18N.format("message.outputting-to-location", outdir.toAbsolutePath()));
// Copy template msi wrapper next to msi file
final Path exePath = msi.getParent().resolve(pkg.packageFileNameWithSuffix());
env.createResource("msiwrapper.exe")
.setCategory(I18N.getString("resource.installer-exe"))
.setPublicName("installer.exe")
.saveToFile(exePath);
new ExecutableRebrander(pkg, env::createResource, resourceLock -> {
// Embed msi in msi wrapper exe.
embedMSI(resourceLock, msi.toAbsolutePath().toString());
}).execute(env, exePath, pkg.icon());
Path dstExePath = outdir.resolve(exePath.getFileName());
Files.copy(exePath, dstExePath, StandardCopyOption.REPLACE_EXISTING);
dstExePath.toFile().setExecutable(true);
Log.verbose(I18N.format("message.output-location", outdir.toAbsolutePath()));
return dstExePath;
}
static native int embedMSI(long resourceLock, String msiPath);
private final WinMsiBundler msiBundler = new WinMsiBundler();
private static native int embedMSI(long resourceLock, String msiPath);
}

View File

@ -0,0 +1,108 @@
/*
* 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. 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.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Objects;
import java.util.function.Consumer;
import jdk.jpackage.internal.PackagingPipeline.PackageTaskID;
import jdk.jpackage.internal.PackagingPipeline.PrimaryTaskID;
import jdk.jpackage.internal.PackagingPipeline.TaskID;
import jdk.jpackage.internal.model.WinExePackage;
final record WinExePackager(BuildEnv env, WinExePackage pkg, Path outputDir, Path msiOutputDir) implements Consumer<PackagingPipeline.Builder> {
WinExePackager {
Objects.requireNonNull(env);
Objects.requireNonNull(pkg);
Objects.requireNonNull(outputDir);
Objects.requireNonNull(msiOutputDir);
}
enum ExePackageTaskID implements TaskID {
RUN_POST_MSI_USER_SCRIPT,
WRAP_MSI_IN_EXE
}
@Override
public void accept(PackagingPipeline.Builder pipelineBuilder) {
pipelineBuilder.excludeDirFromCopying(outputDir)
.task(ExePackageTaskID.RUN_POST_MSI_USER_SCRIPT)
.action(this::runPostMsiScript)
.addDependency(PackageTaskID.CREATE_PACKAGE_FILE)
.add()
.task(ExePackageTaskID.WRAP_MSI_IN_EXE)
.action(this::wrapMsiInExe)
.addDependency(ExePackageTaskID.RUN_POST_MSI_USER_SCRIPT)
.addDependent(PrimaryTaskID.PACKAGE)
.add();
}
private Path msi() {
return msiOutputDir.resolve(pkg.msiPackage().packageFileNameWithSuffix());
}
private void runPostMsiScript() throws IOException {
new ScriptRunner()
.setDirectory(msiOutputDir)
.setResourceCategoryId("resource.post-msi-script")
.setScriptNameSuffix("post-msi")
.setEnvironmentVariable("JpMsiFile", msi().toAbsolutePath().toString())
.run(env, pkg.msiPackage().packageName());
}
private void wrapMsiInExe() throws IOException {
Log.verbose(I18N.format("message.outputting-to-location", outputDir.toAbsolutePath()));
final var msi = msi();
// Copy template msi wrapper next to msi file
final Path exePath = msi.getParent().resolve(pkg.packageFileNameWithSuffix());
env.createResource("msiwrapper.exe")
.setCategory(I18N.getString("resource.installer-exe"))
.setPublicName("installer.exe")
.saveToFile(exePath);
new ExecutableRebrander(pkg, env::createResource, resourceLock -> {
// Embed msi in msi wrapper exe.
WinExeBundler.embedMSI(resourceLock, msi.toAbsolutePath().toString());
}).execute(env, exePath, pkg.icon());
Path dstExePath = outputDir.resolve(exePath.getFileName());
Files.createDirectories(dstExePath.getParent());
Files.copy(exePath, dstExePath, StandardCopyOption.REPLACE_EXISTING);
dstExePath.toFile().setExecutable(true);
Log.verbose(I18N.format("message.output-location", outputDir.toAbsolutePath()));
}
}

View File

@ -31,6 +31,7 @@ import static jdk.jpackage.internal.FromParams.createApplicationBundlerParam;
import static jdk.jpackage.internal.FromParams.createPackageBuilder;
import static jdk.jpackage.internal.FromParams.createPackageBundlerParam;
import static jdk.jpackage.internal.FromParams.findLauncherShortcut;
import static jdk.jpackage.internal.StandardBundlerParam.ICON;
import static jdk.jpackage.internal.StandardBundlerParam.RESOURCE_DIR;
import static jdk.jpackage.internal.WinPackagingPipeline.APPLICATION_LAYOUT;
import static jdk.jpackage.internal.model.StandardPackageType.WIN_MSI;
@ -41,6 +42,7 @@ import java.util.Map;
import java.util.UUID;
import jdk.jpackage.internal.model.ConfigException;
import jdk.jpackage.internal.model.WinApplication;
import jdk.jpackage.internal.model.WinExePackage;
import jdk.jpackage.internal.model.WinLauncher;
import jdk.jpackage.internal.model.WinLauncherMixin;
import jdk.jpackage.internal.model.WinMsiPackage;
@ -99,12 +101,26 @@ final class WinFromParams {
return pkgBuilder.create();
}
private static WinExePackage createWinExePackage(Map<String, ? super Object> params) throws ConfigException, IOException {
final var msiPkg = MSI_PACKAGE.fetchFrom(params);
final var pkgBuilder = new WinExePackageBuilder(msiPkg);
ICON.copyInto(params, pkgBuilder::icon);
return pkgBuilder.create();
}
static final BundlerParamInfo<WinApplication> APPLICATION = createApplicationBundlerParam(
WinFromParams::createWinApplication);
static final BundlerParamInfo<WinMsiPackage> MSI_PACKAGE = createPackageBundlerParam(
WinFromParams::createWinMsiPackage);
static final BundlerParamInfo<WinExePackage> EXE_PACKAGE = createPackageBundlerParam(
WinFromParams::createWinExePackage);
private static final BundlerParamInfo<String> WIN_MENU_HINT = createStringBundlerParam(
Arguments.CLIOptions.WIN_MENU_HINT.getId());

View File

@ -27,115 +27,16 @@ package jdk.jpackage.internal;
import static jdk.jpackage.internal.model.ConfigException.rethrowConfigException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import jdk.jpackage.internal.PackagingPipeline.PackageBuildEnv;
import jdk.jpackage.internal.model.AppImageLayout;
import jdk.jpackage.internal.model.ApplicationLayout;
import jdk.jpackage.internal.model.ConfigException;
import jdk.jpackage.internal.model.Package;
import jdk.jpackage.internal.model.PackagerException;
import jdk.jpackage.internal.model.RuntimeLayout;
import jdk.jpackage.internal.model.WinMsiPackage;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import jdk.jpackage.internal.util.Result;
/**
* WinMsiBundler
*
* Produces .msi installer from application image. Uses WiX Toolkit to build
* .msi installer.
* <p>
* {@link #execute} method creates a number of source files with the description
* of installer to be processed by WiX tools. Generated source files are stored
* in "config" subdirectory next to "app" subdirectory in the root work
* directory. The following WiX source files are generated:
* <ul>
* <li>main.wxs. Main source file with the installer description
* <li>bundle.wxf. Source file with application and Java run-time directory tree
* description.
* <li>ui.wxf. Source file with UI description of the installer.
* </ul>
*
* <p>
* main.wxs file is a copy of main.wxs resource from
* jdk.jpackage.internal.resources package. It is parametrized with the
* following WiX variables:
* <ul>
* <li>JpAppName. Name of the application. Set to the value of --name command
* line option
* <li>JpAppVersion. Version of the application. Set to the value of
* --app-version command line option
* <li>JpAppVendor. Vendor of the application. Set to the value of --vendor
* command line option
* <li>JpAppDescription. Description of the application. Set to the value of
* --description command line option
* <li>JpProductCode. Set to product code UUID of the application. Random value
* generated by jpackage every time {@link #execute} method is called
* <li>JpProductUpgradeCode. Set to upgrade code UUID of the application. Random
* value generated by jpackage every time {@link #execute} method is called if
* --win-upgrade-uuid command line option is not specified. Otherwise this
* variable is set to the value of --win-upgrade-uuid command line option
* <li>JpAllowUpgrades. Set to "yes", but all that matters is it is defined.
* <li>JpAllowDowngrades. Defined for application installers, and undefined for
* Java runtime installers.
* <li>JpConfigDir. Absolute path to the directory with generated WiX source
* files.
* <li>JpIsSystemWide. Set to "yes" if --win-per-user-install command line
* option was not specified. Undefined otherwise
* <li>JpAppSizeKb. Set to estimated size of the application in kilobytes
* <li>JpHelpURL. Set to value of --win-help-url command line option if it
* was specified. Undefined otherwise
* <li>JpAboutURL. Set to value of --about-url command line option if it
* was specified. Undefined otherwise
* <li>JpUpdateURL. Set to value of --win-update-url command line option if it
* was specified. Undefined otherwise
* </ul>
*
* <p>
* ui.wxf file is generated based on --license-file, --win-shortcut-prompt,
* --win-dir-chooser command line options. It is parametrized with the following
* WiX variables:
* <ul>
* <li>JpLicenseRtf. Set to the value of --license-file command line option.
* Undefined if --license-file command line option was not specified
* </ul>
*/
public class WinMsiBundler extends AbstractBundler {
public WinMsiBundler() {
wixFragments = Stream.of(
Map.entry("bundle.wxf", new WixAppImageFragmentBuilder()),
Map.entry("ui.wxf", new WixUiFragmentBuilder()),
Map.entry("os-condition.wxf", OSVersionCondition.createWixFragmentBuilder())
).<WixFragmentBuilder>map(e -> {
e.getValue().setOutputFileName(e.getKey());
return e.getValue();
}).toList();
}
@Override
@ -156,10 +57,12 @@ public class WinMsiBundler extends AbstractBundler {
@Override
public boolean supported(boolean platformInstaller) {
try {
if (wixToolset == null) {
wixToolset = WixTool.createToolset();
try {
sysEnv.orElseThrow();
return true;
} catch (RuntimeException ex) {
ConfigException.rethrowConfigException(ex);
}
return true;
} catch (ConfigException ce) {
Log.error(ce.getMessage());
if (ce.getAdvice() != null) {
@ -184,9 +87,7 @@ public class WinMsiBundler extends AbstractBundler {
WinFromParams.APPLICATION.fetchFrom(params);
BuildEnvFromParams.BUILD_ENV.fetchFrom(params);
if (wixToolset == null) {
wixToolset = WixTool.createToolset();
}
final var wixToolset = sysEnv.orElseThrow().wixToolset();
for (var tool : wixToolset.getType().getTools()) {
Log.verbose(I18N.format("message.tool-version",
@ -194,367 +95,23 @@ public class WinMsiBundler extends AbstractBundler {
wixToolset.getVersion()));
}
wixFragments.forEach(wixFragment -> wixFragment.setWixVersion(wixToolset.getVersion(),
wixToolset.getType()));
wixFragments.stream().map(WixFragmentBuilder::getLoggableWixFeatures).flatMap(
List::stream).distinct().toList().forEach(Log::verbose);
return true;
} catch (RuntimeException re) {
throw rethrowConfigException(re);
}
}
private void prepareProto(Package pkg, BuildEnv env, AppImageLayout appImageLayout) throws
PackagerException, IOException {
// Configure installer icon
if (appImageLayout instanceof RuntimeLayout runtimeLayout) {
// Use icon from java launcher.
// Assume java.exe exists in Java Runtime being packed.
// Ignore custom icon if any as we don't want to copy anything in
// Java Runtime image.
installerIcon = runtimeLayout.runtimeDirectory().resolve(Path.of("bin", "java.exe"));
} else if (appImageLayout instanceof ApplicationLayout appLayout) {
installerIcon = appLayout.launchersDirectory().resolve(
pkg.app().mainLauncher().orElseThrow().executableNameWithSuffix());
}
installerIcon = installerIcon.toAbsolutePath();
pkg.licenseFile().ifPresent(licenseFile -> {
// need to copy license file to the working directory
// and convert to rtf if needed
Path destFile = env.configDir().resolve(licenseFile.getFileName());
try {
IOUtils.copyFile(licenseFile, destFile);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
destFile.toFile().setWritable(true);
ensureByMutationFileIsRTF(destFile);
});
}
@Override
public Path execute(Map<String, ? super Object> params,
Path outputParentDir) throws PackagerException {
IOUtils.writableOutputDir(outputParentDir);
// Order is important!
var pkg = WinFromParams.MSI_PACKAGE.fetchFrom(params);
var env = BuildEnvFromParams.BUILD_ENV.fetchFrom(params);
WinPackagingPipeline.build()
.excludeDirFromCopying(outputParentDir)
.task(PackagingPipeline.PackageTaskID.CREATE_CONFIG_FILES)
.packageAction(this::prepareConfigFiles)
.add()
.task(PackagingPipeline.PackageTaskID.CREATE_PACKAGE_FILE)
.packageAction(this::buildPackage)
.add()
.create().execute(env, pkg, outputParentDir);
return outputParentDir.resolve(pkg.packageFileNameWithSuffix()).toAbsolutePath();
return Packager.<WinMsiPackage>build().outputDir(outputParentDir)
.pkg(WinFromParams.MSI_PACKAGE.fetchFrom(params))
.env(BuildEnvFromParams.BUILD_ENV.fetchFrom(params))
.pipelineBuilderMutatorFactory((env, pkg, outputDir) -> {
return new WinMsiPackager(env, pkg, outputDir, sysEnv.orElseThrow());
}).execute(WinPackagingPipeline.build());
}
private void prepareConfigFiles(PackageBuildEnv<WinMsiPackage, AppImageLayout> env) throws PackagerException, IOException {
prepareProto(env.pkg(), env.env(), env.resolvedLayout());
for (var wixFragment : wixFragments) {
wixFragment.initFromParams(env.env(), env.pkg());
wixFragment.addFilesToConfigRoot();
}
final var msiOut = env.outputDir().resolve(env.pkg().packageFileNameWithSuffix());
Log.verbose(I18N.format("message.preparing-msi-config", msiOut.toAbsolutePath()));
final var wixVars = createWixVars(env);
final var wixObjDir = env.env().buildRoot().resolve("wixobj");
final var configDir = env.env().configDir();
final var wixPipelineBuilder = WixPipeline.build()
.setWixObjDir(wixObjDir)
.setWorkDir(env.env().appImageDir())
.addSource(configDir.resolve("main.wxs"), wixVars);
for (var wixFragment : wixFragments) {
wixFragment.configureWixPipeline(wixPipelineBuilder);
}
switch (wixToolset.getType()) {
case Wix3 -> {
wixPipelineBuilder.addLightOptions("-sice:ICE27");
if (!env.pkg().isSystemWideInstall()) {
wixPipelineBuilder.addLightOptions("-sice:ICE91");
}
}
case Wix4 -> {
}
default -> {
throw new IllegalArgumentException();
}
}
var primaryWxlFiles = Stream.of("de", "en", "ja", "zh_CN").map(loc -> {
return configDir.resolve("MsiInstallerStrings_" + loc + ".wxl");
}).toList();
var wixResources = new WixSourceConverter.ResourceGroup(wixToolset.getType());
// Copy standard l10n files.
for (var path : primaryWxlFiles) {
var name = path.getFileName().toString();
wixResources.addResource(env.env().createResource(name).setPublicName(name).setCategory(
I18N.getString("resource.wxl-file")), path);
}
wixResources.addResource(env.env().createResource("main.wxs").setPublicName("main.wxs").
setCategory(I18N.getString("resource.main-wix-file")), configDir.resolve("main.wxs"));
wixResources.addResource(env.env().createResource("overrides.wxi").setPublicName(
"overrides.wxi").setCategory(I18N.getString("resource.overrides-wix-file")),
configDir.resolve("overrides.wxi"));
// Filter out custom l10n files that were already used to
// override primary l10n files. Ignore case filename comparison,
// both lists are expected to be short.
List<Path> customWxlFiles = env.env().resourceDir()
.map(WinMsiBundler::getWxlFilesFromDir)
.orElseGet(Collections::emptyList)
.stream()
.filter(custom -> primaryWxlFiles.stream().noneMatch(primary ->
primary.getFileName().toString().equalsIgnoreCase(
custom.getFileName().toString())))
.peek(custom -> Log.verbose(I18N.format(
"message.using-custom-resource", String.format("[%s]",
I18N.getString("resource.wxl-file")),
custom.getFileName()))).toList();
// Copy custom l10n files.
for (var path : customWxlFiles) {
var name = path.getFileName().toString();
wixResources.addResource(env.env().createResource(name).setPublicName(name).
setSourceOrder(OverridableResource.Source.ResourceDir).setCategory(I18N.
getString("resource.wxl-file")), configDir.resolve(name));
}
// Save all WiX resources into config dir.
wixResources.saveResources();
// All l10n files are supplied to WiX with "-loc", but only
// Cultures from custom files and a single primary Culture are
// included into "-cultures" list
for (var wxl : primaryWxlFiles) {
wixPipelineBuilder.addLightOptions("-loc", wxl.toString());
}
List<String> cultures = new ArrayList<>();
for (var wxl : customWxlFiles) {
wxl = configDir.resolve(wxl.getFileName());
wixPipelineBuilder.addLightOptions("-loc", wxl.toString());
cultures.add(getCultureFromWxlFile(wxl));
}
// Append a primary culture bases on runtime locale.
final Path primaryWxlFile = configDir.resolve(
I18N.getString("resource.wxl-file-name"));
cultures.add(getCultureFromWxlFile(primaryWxlFile));
// Build ordered list of unique cultures.
Set<String> uniqueCultures = new LinkedHashSet<>();
uniqueCultures.addAll(cultures);
switch (wixToolset.getType()) {
case Wix3 -> {
wixPipelineBuilder.addLightOptions(uniqueCultures.stream().collect(Collectors.joining(";",
"-cultures:", "")));
}
case Wix4 -> {
uniqueCultures.forEach(culture -> {
wixPipelineBuilder.addLightOptions("-culture", culture);
});
}
default -> {
throw new IllegalArgumentException();
}
}
Files.createDirectories(wixObjDir);
wixPipeline = wixPipelineBuilder.create(wixToolset);
}
private void buildPackage(PackageBuildEnv<WinMsiPackage, AppImageLayout> env) throws PackagerException, IOException {
final var msiOut = env.outputDir().resolve(env.pkg().packageFileNameWithSuffix());
Log.verbose(I18N.format("message.generating-msi", msiOut.toAbsolutePath()));
wixPipeline.buildMsi(msiOut.toAbsolutePath());
}
private Map<String, String> createWixVars(PackageBuildEnv<WinMsiPackage, AppImageLayout> env) throws IOException {
Map<String, String> data = new HashMap<>();
final var pkg = env.pkg();
data.put("JpProductCode", pkg.productCode().toString());
data.put("JpProductUpgradeCode", pkg.upgradeCode().toString());
Log.verbose(I18N.format("message.product-code", pkg.productCode()));
Log.verbose(I18N.format("message.upgrade-code", pkg.upgradeCode()));
data.put("JpAllowUpgrades", "yes");
if (!pkg.isRuntimeInstaller()) {
data.put("JpAllowDowngrades", "yes");
}
data.put("JpAppName", pkg.packageName());
data.put("JpAppDescription", pkg.description());
data.put("JpAppVendor", pkg.app().vendor());
data.put("JpAppVersion", pkg.version());
if (Files.exists(installerIcon)) {
data.put("JpIcon", installerIcon.toString());
}
pkg.helpURL().ifPresent(value -> {
data.put("JpHelpURL", value);
});
pkg.updateURL().ifPresent(value -> {
data.put("JpUpdateURL", value);
});
pkg.aboutURL().ifPresent(value -> {
data.put("JpAboutURL", value);
});
data.put("JpAppSizeKb", Long.toString(AppImageLayout.toPathGroup(
env.resolvedLayout()).sizeInBytes() >> 10));
data.put("JpConfigDir", env.env().configDir().toAbsolutePath().toString());
if (pkg.isSystemWideInstall()) {
data.put("JpIsSystemWide", "yes");
}
return data;
}
private static List<Path> getWxlFilesFromDir(Path dir) {
final String glob = "glob:**/*.wxl";
final PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher(
glob);
try (var walk = Files.walk(dir, 1)) {
return walk
.filter(Files::isReadable)
.filter(pathMatcher::matches)
.sorted((a, b) -> a.getFileName().toString().compareToIgnoreCase(b.getFileName().toString()))
.toList();
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
private static String getCultureFromWxlFile(Path wxlPath) {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(false);
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(wxlPath.toFile());
XPath xPath = XPathFactory.newInstance().newXPath();
NodeList nodes = (NodeList) xPath.evaluate(
"//WixLocalization/@Culture", doc, XPathConstants.NODESET);
if (nodes.getLength() != 1) {
throw new IOException(I18N.format(
"error.extract-culture-from-wix-l10n-file",
wxlPath.toAbsolutePath().normalize()));
}
return nodes.item(0).getNodeValue();
} catch (XPathExpressionException | ParserConfigurationException
| SAXException ex) {
throw new UncheckedIOException(new IOException(
I18N.format("error.read-wix-l10n-file", wxlPath.toAbsolutePath().normalize()), ex));
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
private static void ensureByMutationFileIsRTF(Path f) {
try {
boolean existingLicenseIsRTF = false;
try (InputStream fin = Files.newInputStream(f)) {
byte[] firstBits = new byte[7];
if (fin.read(firstBits) == firstBits.length) {
String header = new String(firstBits);
existingLicenseIsRTF = "{\\rtf1\\".equals(header);
}
}
if (!existingLicenseIsRTF) {
List<String> oldLicense = Files.readAllLines(f);
try (Writer w = Files.newBufferedWriter(
f, Charset.forName("Windows-1252"))) {
w.write("{\\rtf1\\ansi\\ansicpg1252\\deff0\\deflang1033"
+ "{\\fonttbl{\\f0\\fnil\\fcharset0 Arial;}}\n"
+ "\\viewkind4\\uc1\\pard\\sa200\\sl276"
+ "\\slmult1\\lang9\\fs20 ");
oldLicense.forEach(l -> {
try {
for (char c : l.toCharArray()) {
// 0x00 <= ch < 0x20 Escaped (\'hh)
// 0x20 <= ch < 0x80 Raw(non - escaped) char
// 0x80 <= ch <= 0xFF Escaped(\ 'hh)
// 0x5C, 0x7B, 0x7D (special RTF characters
// \,{,})Escaped(\'hh)
// ch > 0xff Escaped (\\ud###?)
if (c < 0x10) {
w.write("\\'0");
w.write(Integer.toHexString(c));
} else if (c > 0xff) {
w.write("\\ud");
w.write(Integer.toString(c));
// \\uc1 is in the header and in effect
// so we trail with a replacement char if
// the font lacks that character - '?'
w.write("?");
} else if ((c < 0x20) || (c >= 0x80) ||
(c == 0x5C) || (c == 0x7B) ||
(c == 0x7D)) {
w.write("\\'");
w.write(Integer.toHexString(c));
} else {
w.write(c);
}
}
// blank lines are interpreted as paragraph breaks
if (l.length() < 1) {
w.write("\\par");
} else {
w.write(" ");
}
w.write("\r\n");
} catch (IOException e) {
Log.verbose(e);
}
});
w.write("}\r\n");
}
}
} catch (IOException e) {
Log.verbose(e);
}
}
private Path installerIcon;
private WixToolset wixToolset;
private WixPipeline wixPipeline;
private final List<WixFragmentBuilder> wixFragments;
final Result<WinSystemEnvironment> sysEnv = WinSystemEnvironment.create();
}

View File

@ -0,0 +1,486 @@
/*
* 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. 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.io.InputStream;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import jdk.jpackage.internal.model.AppImageLayout;
import jdk.jpackage.internal.model.PackagerException;
import jdk.jpackage.internal.model.RuntimeLayout;
import jdk.jpackage.internal.model.WinMsiPackage;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
* WinMsiPackager
*
* Produces .msi installer from application image. Uses WiX Toolkit to build
* .msi installer.
* <p>
* Creates a number of source files with the description
* of installer to be processed by WiX tools. Generated source files are stored
* in "config" subdirectory next to "app" subdirectory in the root work
* directory. The following WiX source files are generated:
* <ul>
* <li>main.wxs. Main source file with the installer description
* <li>bundle.wxf. Source file with application and Java run-time directory tree
* description.
* <li>ui.wxf. Source file with UI description of the installer.
* </ul>
*
* <p>
* main.wxs file is a copy of main.wxs resource from
* jdk.jpackage.internal.resources package. It is parametrized with the
* following WiX variables:
* <ul>
* <li>JpAppName. Name of the application. Set to the value of --name command
* line option
* <li>JpAppVersion. Version of the application. Set to the value of
* --app-version command line option
* <li>JpAppVendor. Vendor of the application. Set to the value of --vendor
* command line option
* <li>JpAppDescription. Description of the application. Set to the value of
* --description command line option
* <li>JpProductCode. Set to product code UUID of the application. Random value
* generated by jpackage every time {@link #execute} method is called
* <li>JpProductUpgradeCode. Set to upgrade code UUID of the application. Random
* value generated by jpackage every time {@link #execute} method is called if
* --win-upgrade-uuid command line option is not specified. Otherwise this
* variable is set to the value of --win-upgrade-uuid command line option
* <li>JpAllowUpgrades. Set to "yes", but all that matters is it is defined.
* <li>JpAllowDowngrades. Defined for application installers, and undefined for
* Java runtime installers.
* <li>JpConfigDir. Absolute path to the directory with generated WiX source
* files.
* <li>JpIsSystemWide. Set to "yes" if --win-per-user-install command line
* option was not specified. Undefined otherwise
* <li>JpAppSizeKb. Set to estimated size of the application in kilobytes
* <li>JpHelpURL. Set to value of --win-help-url command line option if it
* was specified. Undefined otherwise
* <li>JpAboutURL. Set to value of --about-url command line option if it
* was specified. Undefined otherwise
* <li>JpUpdateURL. Set to value of --win-update-url command line option if it
* was specified. Undefined otherwise
* </ul>
*
* <p>
* ui.wxf file is generated based on --license-file, --win-shortcut-prompt,
* --win-dir-chooser command line options. It is parametrized with the following
* WiX variables:
* <ul>
* <li>JpLicenseRtf. Set to the value of --license-file command line option.
* Undefined if --license-file command line option was not specified
* </ul>
*/
final class WinMsiPackager implements Consumer<PackagingPipeline.Builder> {
WinMsiPackager(BuildEnv env, WinMsiPackage pkg, Path outputDir, WixToolset wixToolset) {
this.pkg = Objects.requireNonNull(pkg);
this.env = Objects.requireNonNull(env);
this.outputDir = Objects.requireNonNull(outputDir);
this.wixToolset = Objects.requireNonNull(wixToolset);
wixFragments = Stream.of(
Map.entry("bundle.wxf", new WixAppImageFragmentBuilder()),
Map.entry("ui.wxf", new WixUiFragmentBuilder()),
Map.entry("os-condition.wxf", OSVersionCondition.createWixFragmentBuilder())
).<WixFragmentBuilder>map(e -> {
e.getValue().setOutputFileName(e.getKey());
return e.getValue();
}).toList();
// Configure installer icon
if (env.appImageLayout() instanceof RuntimeLayout runtimeLayout) {
// Use icon from java launcher.
// Assume java.exe exists in Java Runtime being packed.
// Ignore custom icon if any as we don't want to copy anything in
// Java Runtime image.
installerIcon = runtimeLayout.runtimeDirectory().resolve(Path.of("bin", "java.exe")).toAbsolutePath();
} else {
installerIcon = env.asApplicationLayout().orElseThrow().launchersDirectory().resolve(
pkg.app().mainLauncher().orElseThrow().executableNameWithSuffix()).toAbsolutePath();
}
wixFragments.forEach(wixFragment -> wixFragment.setWixVersion(wixToolset.getVersion(),
wixToolset.getType()));
wixFragments.stream().map(WixFragmentBuilder::getLoggableWixFeatures).flatMap(
List::stream).distinct().toList().forEach(Log::verbose);
}
WinMsiPackager(BuildEnv env, WinMsiPackage pkg, Path outputDir, WinSystemEnvironment sysEnv) {
this(env, pkg, outputDir, sysEnv.wixToolset());
}
@Override
public void accept(PackagingPipeline.Builder pipelineBuilder) {
pipelineBuilder.excludeDirFromCopying(outputDir)
.task(PackagingPipeline.PackageTaskID.CREATE_CONFIG_FILES)
.action(this::prepareConfigFiles)
.add()
.task(PackagingPipeline.PackageTaskID.CREATE_PACKAGE_FILE)
.action(this::buildPackage)
.add();
}
private void prepareConfigFiles() throws PackagerException, IOException {
pkg.licenseFile().ifPresent(licenseFile -> {
// need to copy license file to the working directory
// and convert to rtf if needed
Path destFile = env.configDir().resolve(licenseFile.getFileName());
try {
IOUtils.copyFile(licenseFile, destFile);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
destFile.toFile().setWritable(true);
ensureByMutationFileIsRTF(destFile);
});
for (var wixFragment : wixFragments) {
wixFragment.initFromParams(env, pkg);
wixFragment.addFilesToConfigRoot();
}
final var msiOut = outputDir.resolve(pkg.packageFileNameWithSuffix());
Log.verbose(I18N.format("message.preparing-msi-config", msiOut.toAbsolutePath()));
final var wixVars = createWixVars();
final var wixObjDir = env.buildRoot().resolve("wixobj");
final var configDir = env.configDir();
final var wixPipelineBuilder = WixPipeline.build()
.setWixObjDir(wixObjDir)
.setWorkDir(env.appImageDir())
.addSource(configDir.resolve("main.wxs"), wixVars);
for (var wixFragment : wixFragments) {
wixFragment.configureWixPipeline(wixPipelineBuilder);
}
switch (wixToolset.getType()) {
case Wix3 -> {
wixPipelineBuilder.addLightOptions("-sice:ICE27");
if (!pkg.isSystemWideInstall()) {
wixPipelineBuilder.addLightOptions("-sice:ICE91");
}
}
case Wix4 -> {
}
default -> {
throw new IllegalArgumentException();
}
}
var primaryWxlFiles = Stream.of("de", "en", "ja", "zh_CN").map(loc -> {
return configDir.resolve("MsiInstallerStrings_" + loc + ".wxl");
}).toList();
var wixResources = new WixSourceConverter.ResourceGroup(wixToolset.getType());
// Copy standard l10n files.
for (var path : primaryWxlFiles) {
var name = path.getFileName().toString();
wixResources.addResource(env.createResource(name).setPublicName(name).setCategory(
I18N.getString("resource.wxl-file")), path);
}
wixResources.addResource(env.createResource("main.wxs").setPublicName("main.wxs").
setCategory(I18N.getString("resource.main-wix-file")), configDir.resolve("main.wxs"));
wixResources.addResource(env.createResource("overrides.wxi").setPublicName(
"overrides.wxi").setCategory(I18N.getString("resource.overrides-wix-file")),
configDir.resolve("overrides.wxi"));
// Filter out custom l10n files that were already used to
// override primary l10n files. Ignore case filename comparison,
// both lists are expected to be short.
List<Path> customWxlFiles = env.resourceDir()
.map(WinMsiPackager::getWxlFilesFromDir)
.orElseGet(Collections::emptyList)
.stream()
.filter(custom -> primaryWxlFiles.stream().noneMatch(primary ->
primary.getFileName().toString().equalsIgnoreCase(
custom.getFileName().toString())))
.peek(custom -> Log.verbose(I18N.format(
"message.using-custom-resource", String.format("[%s]",
I18N.getString("resource.wxl-file")),
custom.getFileName()))).toList();
// Copy custom l10n files.
for (var path : customWxlFiles) {
var name = path.getFileName().toString();
wixResources.addResource(env.createResource(name).setPublicName(name).
setSourceOrder(OverridableResource.Source.ResourceDir).setCategory(I18N.
getString("resource.wxl-file")), configDir.resolve(name));
}
// Save all WiX resources into config dir.
wixResources.saveResources();
// All l10n files are supplied to WiX with "-loc", but only
// Cultures from custom files and a single primary Culture are
// included into "-cultures" list
for (var wxl : primaryWxlFiles) {
wixPipelineBuilder.addLightOptions("-loc", wxl.toString());
}
List<String> cultures = new ArrayList<>();
for (var wxl : customWxlFiles) {
wxl = configDir.resolve(wxl.getFileName());
wixPipelineBuilder.addLightOptions("-loc", wxl.toString());
cultures.add(getCultureFromWxlFile(wxl));
}
// Append a primary culture bases on runtime locale.
final Path primaryWxlFile = configDir.resolve(
I18N.getString("resource.wxl-file-name"));
cultures.add(getCultureFromWxlFile(primaryWxlFile));
// Build ordered list of unique cultures.
Set<String> uniqueCultures = new LinkedHashSet<>();
uniqueCultures.addAll(cultures);
switch (wixToolset.getType()) {
case Wix3 -> {
wixPipelineBuilder.addLightOptions(uniqueCultures.stream().collect(Collectors.joining(";",
"-cultures:", "")));
}
case Wix4 -> {
uniqueCultures.forEach(culture -> {
wixPipelineBuilder.addLightOptions("-culture", culture);
});
}
default -> {
throw new IllegalArgumentException();
}
}
Files.createDirectories(wixObjDir);
wixPipeline = wixPipelineBuilder.create(wixToolset);
}
private void buildPackage() throws PackagerException, IOException {
final var msiOut = outputDir.resolve(pkg.packageFileNameWithSuffix());
Log.verbose(I18N.format("message.generating-msi", msiOut.toAbsolutePath()));
wixPipeline.buildMsi(msiOut.toAbsolutePath());
}
private Map<String, String> createWixVars() throws IOException {
Map<String, String> data = new HashMap<>();
data.put("JpProductCode", pkg.productCode().toString());
data.put("JpProductUpgradeCode", pkg.upgradeCode().toString());
Log.verbose(I18N.format("message.product-code", pkg.productCode()));
Log.verbose(I18N.format("message.upgrade-code", pkg.upgradeCode()));
data.put("JpAllowUpgrades", "yes");
if (!pkg.isRuntimeInstaller()) {
data.put("JpAllowDowngrades", "yes");
}
data.put("JpAppName", pkg.packageName());
data.put("JpAppDescription", pkg.description());
data.put("JpAppVendor", pkg.app().vendor());
data.put("JpAppVersion", pkg.version());
if (Files.exists(installerIcon)) {
data.put("JpIcon", installerIcon.toString());
}
pkg.helpURL().ifPresent(value -> {
data.put("JpHelpURL", value);
});
pkg.updateURL().ifPresent(value -> {
data.put("JpUpdateURL", value);
});
pkg.aboutURL().ifPresent(value -> {
data.put("JpAboutURL", value);
});
data.put("JpAppSizeKb", Long.toString(AppImageLayout.toPathGroup(
env.appImageLayout()).sizeInBytes() >> 10));
data.put("JpConfigDir", env.configDir().toAbsolutePath().toString());
if (pkg.isSystemWideInstall()) {
data.put("JpIsSystemWide", "yes");
}
return data;
}
private static List<Path> getWxlFilesFromDir(Path dir) {
final String glob = "glob:**/*.wxl";
final PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher(
glob);
try (var walk = Files.walk(dir, 1)) {
return walk
.filter(Files::isReadable)
.filter(pathMatcher::matches)
.sorted((a, b) -> a.getFileName().toString().compareToIgnoreCase(b.getFileName().toString()))
.toList();
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
private static String getCultureFromWxlFile(Path wxlPath) {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(false);
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(wxlPath.toFile());
XPath xPath = XPathFactory.newInstance().newXPath();
NodeList nodes = (NodeList) xPath.evaluate(
"//WixLocalization/@Culture", doc, XPathConstants.NODESET);
if (nodes.getLength() != 1) {
throw new RuntimeException(I18N.format(
"error.extract-culture-from-wix-l10n-file",
wxlPath.toAbsolutePath().normalize()));
}
return nodes.item(0).getNodeValue();
} catch (XPathExpressionException | ParserConfigurationException | SAXException ex) {
throw new RuntimeException(I18N.format(
"error.read-wix-l10n-file", wxlPath.toAbsolutePath().normalize()), ex);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
private static void ensureByMutationFileIsRTF(Path f) {
try {
boolean existingLicenseIsRTF = false;
try (InputStream fin = Files.newInputStream(f)) {
byte[] firstBits = new byte[7];
if (fin.read(firstBits) == firstBits.length) {
String header = new String(firstBits);
existingLicenseIsRTF = "{\\rtf1\\".equals(header);
}
}
if (!existingLicenseIsRTF) {
List<String> oldLicense = Files.readAllLines(f);
try (Writer w = Files.newBufferedWriter(
f, Charset.forName("Windows-1252"))) {
w.write("{\\rtf1\\ansi\\ansicpg1252\\deff0\\deflang1033"
+ "{\\fonttbl{\\f0\\fnil\\fcharset0 Arial;}}\n"
+ "\\viewkind4\\uc1\\pard\\sa200\\sl276"
+ "\\slmult1\\lang9\\fs20 ");
oldLicense.forEach(l -> {
try {
for (char c : l.toCharArray()) {
// 0x00 <= ch < 0x20 Escaped (\'hh)
// 0x20 <= ch < 0x80 Raw(non - escaped) char
// 0x80 <= ch <= 0xFF Escaped(\ 'hh)
// 0x5C, 0x7B, 0x7D (special RTF characters
// \,{,})Escaped(\'hh)
// ch > 0xff Escaped (\\ud###?)
if (c < 0x10) {
w.write("\\'0");
w.write(Integer.toHexString(c));
} else if (c > 0xff) {
w.write("\\ud");
w.write(Integer.toString(c));
// \\uc1 is in the header and in effect
// so we trail with a replacement char if
// the font lacks that character - '?'
w.write("?");
} else if ((c < 0x20) || (c >= 0x80) ||
(c == 0x5C) || (c == 0x7B) ||
(c == 0x7D)) {
w.write("\\'");
w.write(Integer.toHexString(c));
} else {
w.write(c);
}
}
// blank lines are interpreted as paragraph breaks
if (l.length() < 1) {
w.write("\\par");
} else {
w.write(" ");
}
w.write("\r\n");
} catch (IOException e) {
Log.verbose(e);
}
});
w.write("}\r\n");
}
}
} catch (IOException e) {
Log.verbose(e);
}
}
private final WinMsiPackage pkg;
private final BuildEnv env;
private final Path outputDir;
private final WixToolset wixToolset;
private final List<WixFragmentBuilder> wixFragments;
private final Path installerIcon;
private WixPipeline wixPipeline;
}

View File

@ -0,0 +1,41 @@
/*
* 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. 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 static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier;
import java.util.Objects;
import jdk.jpackage.internal.util.Result;
record WinSystemEnvironment(WixToolset wixToolset) implements SystemEnvironment {
WinSystemEnvironment {
Objects.requireNonNull(wixToolset);
}
static Result<WinSystemEnvironment> create() {
return Result.create(toSupplier(WixTool::createToolset)).map(WinSystemEnvironment::new);
}
}

View File

@ -183,13 +183,12 @@ public enum WixTool {
final boolean[] tooOld = new boolean[1];
final String[] parsedVersion = new String[1];
final var validator = new ToolValidator(toolPath).setMinimalVersion(tool.minimalVersion).
setToolNotFoundErrorHandler((name, ex) -> {
return new ConfigException("", "");
}).setToolOldVersionErrorHandler((name, version) -> {
tooOld[0] = true;
return null;
});
final var validator = new ToolValidator(toolPath)
.setMinimalVersion(tool.minimalVersion)
.setToolOldVersionErrorHandler((name, version) -> {
tooOld[0] = true;
return null;
});
final Function<Stream<String>, String> versionParser;

View File

@ -23,33 +23,141 @@
package jdk.jpackage.internal;
import jdk.jpackage.internal.model.DottedVersion;
import jdk.jpackage.internal.model.ConfigException;
import java.nio.file.Path;
import jdk.internal.util.OperatingSystem;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import java.nio.file.Path;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import jdk.internal.util.OperatingSystem;
import jdk.jpackage.internal.model.ConfigException;
import jdk.jpackage.internal.model.DottedVersion;
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.EnumSource;
import org.junit.jupiter.params.provider.ValueSource;
public class ToolValidatorTest {
@Test
public void testAvailable() {
assertNull(new ToolValidator(TOOL_JAVA).validate());
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void testAvailable(boolean checkExistsOnly) {
assertNull(new ToolValidator(TOOL_JAVA).checkExistsOnly(checkExistsOnly).validate());
}
@Test
public void testNotAvailable() {
assertValidationFailure(new ToolValidator(TOOL_UNKNOWN).validate(), true);
public void testAvailable_setCommandLine() {
// java doesn't recognize "--foo" command line option, but the validation will
// still pass as there is no minimal version specified and the validator ignores
// the exit code
assertNull(new ToolValidator(TOOL_JAVA).setCommandLine("--foo").validate());
}
enum TestAvailableMode {
NO_VERSION(null),
TOO_OLD("0.9"),
EQUALS("1.0"),
NEWER("1.1");
TestAvailableMode(String parsedVersion) {
this.parsedVersion = parsedVersion;
}
final String parsedVersion;
}
@ParameterizedTest
@EnumSource(TestAvailableMode.class)
public void testAvailable(TestAvailableMode mode) {
var minVer = TestAvailableMode.EQUALS.parsedVersion;
var err = new ToolValidator(TOOL_JAVA).setVersionParser(lines -> {
return mode.parsedVersion;
}).setMinimalVersion(DottedVersion.greedy(minVer)).validate();
if (Set.of(TestAvailableMode.NO_VERSION, TestAvailableMode.TOO_OLD).contains(mode)) {
var expectedMessage = I18N.format("error.tool-old-version", TOOL_JAVA, minVer);
var expectedAdvice = I18N.format("error.tool-old-version.advice", TOOL_JAVA, minVer);
assertEquals(expectedMessage, err.getMessage());
assertEquals(expectedAdvice, err.getAdvice());
} else {
assertNull(err);
}
}
@ParameterizedTest
@EnumSource(TestAvailableMode.class)
public void testAvailable_setToolOldVersionErrorHandler(TestAvailableMode mode) {
var handler = new ToolOldVersionErrorHandler();
var minVer = TestAvailableMode.EQUALS.parsedVersion;
var err = new ToolValidator(TOOL_JAVA).setVersionParser(lines -> {
return mode.parsedVersion;
}).setMinimalVersion(DottedVersion.greedy(minVer)).setToolOldVersionErrorHandler(handler).validate();
if (Set.of(TestAvailableMode.NO_VERSION, TestAvailableMode.TOO_OLD).contains(mode)) {
assertSame(ToolOldVersionErrorHandler.ERR, err);
handler.verifyCalled(Path.of(TOOL_JAVA), mode.parsedVersion);
} else {
assertNull(err);
handler.verifyNotCalled();
}
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void testNotAvailable(boolean checkExistsOnly, @TempDir Path dir) {
var err = new ToolValidator(dir.resolve("foo")).checkExistsOnly(checkExistsOnly).validate();
if (checkExistsOnly) {
assertValidationFailure(err, false);
} else {
assertValidationFailureNoAdvice(err, !checkExistsOnly);
}
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void testToolIsDirectory(boolean checkExistsOnly, @TempDir Path dir) {
var err = new ToolValidator(dir).checkExistsOnly(checkExistsOnly).validate();
assertValidationFailureNoAdvice(err, !checkExistsOnly);
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void testNotAvailable_setToolNotFoundErrorHandler(boolean checkExistsOnly, @TempDir Path dir) {
var handler = new ToolNotFoundErrorHandler();
var err = new ToolValidator(dir.resolve("foo")).checkExistsOnly(checkExistsOnly)
.setToolNotFoundErrorHandler(handler)
.validate();
if (checkExistsOnly) {
handler.verifyCalled(dir.resolve("foo"));
assertSame(ToolNotFoundErrorHandler.ERR, err);
} else {
handler.verifyNotCalled();
assertValidationFailureNoAdvice(err, !checkExistsOnly);
}
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void testToolIsDirectory_setToolNotFoundErrorHandler(boolean checkExistsOnly, @TempDir Path dir) {
var handler = new ToolNotFoundErrorHandler();
var err = new ToolValidator(dir).checkExistsOnly(checkExistsOnly).validate();
handler.verifyNotCalled();
assertValidationFailureNoAdvice(err, !checkExistsOnly);
}
@Test
public void testVersionParserUsage() {
// Without minimal version configured, version parser should not be used
new ToolValidator(TOOL_JAVA).setVersionParser(unused -> {
throw new RuntimeException();
throw new AssertionError();
}).validate();
// Minimal version is 1, actual is 10. Should be OK.
@ -81,9 +189,68 @@ public class ToolValidatorTest {
}
}
private static void assertValidationFailureNoAdvice(ConfigException v, boolean withCause) {
assertNotNull(v);
assertNotEquals("", v.getMessage().strip());
assertNull(v.getAdvice());
if (withCause) {
assertNotNull(v.getCause());
} else {
assertNull(v.getCause());
}
}
private static final class ToolNotFoundErrorHandler implements Function<Path, ConfigException> {
@Override
public ConfigException apply(Path tool) {
assertNotNull(tool);
this.tool = tool;
return ERR;
}
void verifyCalled(Path expectedTool) {
assertEquals(Objects.requireNonNull(expectedTool), tool);
}
void verifyNotCalled() {
assertNull(tool);
}
private Path tool;
static final ConfigException ERR = new ConfigException("no tool", "install the tool");
}
private static final class ToolOldVersionErrorHandler implements BiFunction<Path, String, ConfigException> {
@Override
public ConfigException apply(Path tool, String parsedVersion) {
assertNotNull(tool);
this.tool = tool;
this.parsedVersion = parsedVersion;
return ERR;
}
void verifyCalled(Path expectedTool, String expectedParsedVersion) {
assertEquals(Objects.requireNonNull(expectedTool), tool);
assertEquals(expectedParsedVersion, parsedVersion);
}
void verifyNotCalled() {
assertNull(tool);
}
private Path tool;
private String parsedVersion;
static final ConfigException ERR = new ConfigException("tool too old", "install the newer version");
}
private static final String TOOL_JAVA;
private static final String TOOL_UNKNOWN = Path.of(System.getProperty(
"java.home"), "bin").toString();
static {
String fname = "java";