8378877: jpackage: improve rebranding of exe files on Windows

Reviewed-by: almatvee
This commit is contained in:
Alexey Semenyuk 2026-03-03 19:09:03 +00:00
parent 86800eb2b3
commit c13fdc044d
4 changed files with 87 additions and 258 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2020, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -40,9 +40,11 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Stream;
import jdk.jpackage.internal.model.DottedVersion;
import jdk.jpackage.internal.model.JPackageException;
import jdk.jpackage.internal.model.WinApplication;
import jdk.jpackage.internal.model.WinExePackage;
import jdk.jpackage.internal.model.WinLauncher;
@ -74,11 +76,11 @@ final class ExecutableRebrander {
this.props = new HashMap<>();
validateValueAndPut(this.props, Map.entry("COMPANY_NAME", props.vendor), "vendor");
validateValueAndPut(this.props, Map.entry("FILE_DESCRIPTION",props.description), "description");
validateValueAndPut(this.props, Map.entry("FILE_VERSION", props.version.toString()), "version");
validateValueAndPut(this.props, Map.entry("LEGAL_COPYRIGHT", props.copyright), "copyright");
validateValueAndPut(this.props, Map.entry("PRODUCT_NAME", props.name), "name");
this.props.put("COMPANY_NAME", validateSingleLine(props.vendor));
this.props.put("FILE_DESCRIPTION", validateSingleLine(props.description));
this.props.put("FILE_VERSION", validateSingleLine(props.version.toString()));
this.props.put("LEGAL_COPYRIGHT", validateSingleLine(props.copyright));
this.props.put("PRODUCT_NAME", validateSingleLine(props.name));
this.props.put("FIXEDFILEINFO_FILE_VERSION", toFixedFileVersion(props.version));
this.props.put("INTERNAL_NAME", props.executableName);
@ -90,7 +92,7 @@ final class ExecutableRebrander {
UpdateResourceAction versionSwapper = resourceLock -> {
if (versionSwap(resourceLock, propsArray) != 0) {
throw I18N.buildException().message("error.version-swap", target).create(RuntimeException::new);
throw new JPackageException(I18N.format("error.version-swap", target));
}
};
@ -100,7 +102,7 @@ final class ExecutableRebrander {
.map(absIcon -> {
return resourceLock -> {
if (iconSwap(resourceLock, absIcon.toString()) != 0) {
throw I18N.buildException().message("error.icon-swap", absIcon).create(RuntimeException::new);
throw new JPackageException(I18N.format("error.icon-swap", absIcon));
}
};
});
@ -118,43 +120,58 @@ final class ExecutableRebrander {
private static void rebrandExecutable(BuildEnv env, final Path target,
List<UpdateResourceAction> actions) throws IOException {
Objects.requireNonNull(env);
Objects.requireNonNull(target);
Objects.requireNonNull(actions);
actions.forEach(Objects::requireNonNull);
String tempDirectory = env.buildRoot().toAbsolutePath().toString();
if (WindowsDefender.isThereAPotentialWindowsDefenderIssue(tempDirectory)) {
Log.verbose(I18N.format("message.potential.windows.defender.issue", tempDirectory));
}
var shortTargetPath = ShortPathUtils.toShortPath(target);
long resourceLock = lockResource(shortTargetPath.orElse(target).toString());
if (resourceLock == 0) {
throw I18N.buildException().message("error.lock-resource", shortTargetPath.orElse(target)).create(RuntimeException::new);
}
final boolean resourceUnlockedSuccess;
try {
for (var action : actions) {
action.editResource(resourceLock);
}
} finally {
if (resourceLock == 0) {
resourceUnlockedSuccess = true;
} else {
resourceUnlockedSuccess = unlockResource(resourceLock);
if (shortTargetPath.isPresent()) {
// Windows will rename the executable in the unlock operation.
// Should restore executable's name.
var tmpPath = target.getParent().resolve(
target.getFileName().toString() + ".restore");
Files.move(shortTargetPath.get(), tmpPath);
Files.move(tmpPath, target);
}
}
}
Globals.instance().objectFactory().<Void, RuntimeException>retryExecutor(RuntimeException.class).setExecutable(() -> {
if (!resourceUnlockedSuccess) {
throw I18N.buildException().message("error.unlock-resource", target).create(RuntimeException::new);
var shortTargetPath = ShortPathUtils.toShortPath(target);
long resourceLock = lockResource(shortTargetPath.orElse(target).toString());
if (resourceLock == 0) {
throw new JPackageException(I18N.format("error.lock-resource", shortTargetPath.orElse(target)));
}
final boolean resourceUnlockedSuccess;
try {
for (var action : actions) {
try {
action.editResource(resourceLock);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
} finally {
if (resourceLock == 0) {
resourceUnlockedSuccess = true;
} else {
resourceUnlockedSuccess = unlockResource(resourceLock);
if (shortTargetPath.isPresent()) {
// Windows will rename the executable in the unlock operation.
// Should restore executable's name.
var tmpPath = target.getParent().resolve(
target.getFileName().toString() + ".restore");
try {
Files.move(shortTargetPath.get(), tmpPath);
Files.move(tmpPath, target);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
}
}
if (!resourceUnlockedSuccess) {
throw new JPackageException(I18N.format("error.unlock-resource", shortTargetPath.orElse(target)));
}
return null;
}).setMaxAttemptsCount(5).setAttemptTimeout(3, TimeUnit.SECONDS).execute();
} catch (UncheckedIOException ex) {
throw ex.getCause();
}
}
@ -197,14 +214,13 @@ final class ExecutableRebrander {
}
}
private static void validateValueAndPut(Map<String, String> target,
Map.Entry<String, String> e, String label) {
if (e.getValue().contains("\r") || e.getValue().contains("\n")) {
Log.error("Configuration parameter " + label
+ " contains multiple lines of text, ignore it");
e = Map.entry(e.getKey(), "");
private static String validateSingleLine(String v) {
Objects.requireNonNull(v);
if (v.contains("\r") || v.contains("\n")) {
throw new IllegalArgumentException("Configuration parameter contains multiple lines of text");
} else {
return v;
}
target.put(e.getKey(), e.getValue());
}
@FunctionalInterface
@ -212,19 +228,35 @@ final class ExecutableRebrander {
public void editResource(long resourceLock) throws IOException;
}
private static record ExecutableProperties(String vendor, String description,
private record ExecutableProperties(String vendor, String description,
DottedVersion version, String copyright, String name, String executableName) {
static ExecutableProperties create(WinApplication app,
WinLauncher launcher) {
return new ExecutableProperties(app.vendor(), launcher.description(),
app.winVersion(), app.copyright(), launcher.name(),
ExecutableProperties {
Objects.requireNonNull(vendor);
Objects.requireNonNull(description);
Objects.requireNonNull(version);
Objects.requireNonNull(copyright);
Objects.requireNonNull(name);
Objects.requireNonNull(executableName);
}
static ExecutableProperties create(WinApplication app, WinLauncher launcher) {
return new ExecutableProperties(
app.vendor(),
launcher.description(),
app.winVersion(),
app.copyright(),
launcher.name(),
launcher.executableNameWithSuffix());
}
static ExecutableProperties create(WinExePackage pkg) {
return new ExecutableProperties(pkg.app().vendor(),
pkg.description(), DottedVersion.lazy(pkg.version()),
pkg.app().copyright(), pkg.packageName(),
return new ExecutableProperties(
pkg.app().vendor(),
pkg.description(),
DottedVersion.lazy(pkg.version()),
pkg.app().copyright(),
pkg.packageName(),
pkg.packageFileNameWithSuffix());
}
}

View File

@ -1,72 +0,0 @@
/*
* Copyright (c) 2012, 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal;
import java.util.List;
import jdk.internal.util.OperatingSystem;
import jdk.internal.util.OSVersion;
final class WindowsDefender {
private WindowsDefender() {}
static final boolean isThereAPotentialWindowsDefenderIssue(String dir) {
boolean result = false;
if (OperatingSystem.isWindows() &&
OSVersion.current().major() == 10) {
// If DisableRealtimeMonitoring is not enabled then there
// may be a problem.
if (!WindowsRegistry.readDisableRealtimeMonitoring() &&
!isDirectoryInExclusionPath(dir)) {
result = true;
}
}
return result;
}
private static boolean isDirectoryInExclusionPath(String dir) {
boolean result = false;
// If the user temp directory is not found in the exclusion
// list then there may be a problem.
List<String> paths = WindowsRegistry.readExclusionsPaths();
for (String s : paths) {
if (WindowsRegistry.comparePaths(s, dir)) {
result = true;
break;
}
}
return result;
}
static final String getUserTempDirectory() {
String tempDirectory = System.getProperty("java.io.tmpdir");
return tempDirectory;
}
}

View File

@ -1,130 +0,0 @@
/*
* Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal;
import java.util.ArrayList;
import java.util.List;
@SuppressWarnings("restricted")
final class WindowsRegistry {
// Currently we only support HKEY_LOCAL_MACHINE. Native implementation will
// require support for additinal HKEY if needed.
private static final int HKEY_LOCAL_MACHINE = 1;
static {
System.loadLibrary("jpackage");
}
private WindowsRegistry() {}
/**
* Reads the registry value for DisableRealtimeMonitoring.
* @return true if DisableRealtimeMonitoring is set to 0x1,
* false otherwise.
*/
static final boolean readDisableRealtimeMonitoring() {
final String subKey = "Software\\Microsoft\\"
+ "Windows Defender\\Real-Time Protection";
final String value = "DisableRealtimeMonitoring";
int result = readDwordValue(HKEY_LOCAL_MACHINE, subKey, value, 0);
return (result == 1);
}
static final List<String> readExclusionsPaths() {
List<String> result = new ArrayList<>();
final String subKey = "Software\\Microsoft\\"
+ "Windows Defender\\Exclusions\\Paths";
long lKey = openRegistryKey(HKEY_LOCAL_MACHINE, subKey);
if (lKey == 0) {
return result;
}
String valueName;
int index = 0;
do {
valueName = enumRegistryValue(lKey, index);
if (valueName != null) {
result.add(valueName);
index++;
}
} while (valueName != null);
closeRegistryKey(lKey);
return result;
}
/**
* Reads DWORD registry value.
*
* @param key one of HKEY predefine value
* @param subKey registry sub key
* @param value value to read
* @param defaultValue default value in case if subKey or value not found
* or any other errors occurred
* @return value's data only if it was read successfully, otherwise
* defaultValue
*/
private static native int readDwordValue(int key, String subKey,
String value, int defaultValue);
/**
* Open registry key.
*
* @param key one of HKEY predefine value
* @param subKey registry sub key
* @return native handle to open key
*/
private static native long openRegistryKey(int key, String subKey);
/**
* Enumerates the values for registry key.
*
* @param lKey native handle to open key returned by openRegistryKey
* @param index index of value starting from 0. Increment until this
* function returns NULL which means no more values.
* @return returns value or NULL if error or no more data
*/
private static native String enumRegistryValue(long lKey, int index);
/**
* Close registry key.
*
* @param lKey native handle to open key returned by openRegistryKey
*/
private static native void closeRegistryKey(long lKey);
/**
* Compares two Windows paths regardless case and if paths
* are short or long.
*
* @param path1 path to compare
* @param path2 path to compare
* @return true if paths point to same location
*/
public static native boolean comparePaths(String path1, String path2);
}

View File

@ -55,7 +55,6 @@ error.missing-service-installer='service-installer.exe' service installer not fo
error.missing-service-installer.advice=Add 'service-installer.exe' service installer to the resource directory
message.icon-not-ico=The specified icon "{0}" is not an ICO file and will not be used. The default icon will be used in it's place.
message.potential.windows.defender.issue=Warning: Windows Defender may prevent jpackage from functioning. If there is an issue, it can be addressed by either disabling realtime monitoring, or adding an exclusion for the directory "{0}".
message.tool-version=Detected [{0}] version [{1}].
message.wrong-tool-version=Detected [{0}] version {1} but version {2} is required.
message.use-wix36-features=WiX {0} detected. Enabling advanced cleanup action.