Skip to content

Commit 267d69b

Browse files
author
Alexey Semenyuk
committed
8326447: jpackage creates Windows installers that cannot be signed
Reviewed-by: almatvee
1 parent 2efb033 commit 267d69b

File tree

7 files changed

+463
-199
lines changed

7 files changed

+463
-199
lines changed

src/jdk.jpackage/share/man/jpackage.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,12 @@ jpackage will lookup files by specific names in the resource directory.
686686

687687
: A Windows Script File (WSF) to run after building embedded MSI installer for EXE installer
688688

689+
`installer.exe`
690+
691+
: Executable wrapper for MSI installer
692+
693+
Default resource is *msiwrapper.exe*
694+
689695

690696
### Resource directory files considered only when running on macOS:
691697

src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinExeBundler.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2017, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -24,6 +24,8 @@
2424
*/
2525
package jdk.jpackage.internal;
2626

27+
import static jdk.jpackage.internal.OverridableResource.createResource;
28+
2729
import java.io.IOException;
2830
import java.io.InputStream;
2931
import java.nio.file.Files;
@@ -129,9 +131,11 @@ private Path buildEXE(Map<String, ? super Object> params, Path msi,
129131

130132
// Copy template msi wrapper next to msi file
131133
final Path exePath = PathUtils.replaceSuffix(msi, ".exe");
132-
try (InputStream is = OverridableResource.readDefault(EXE_WRAPPER_NAME)) {
133-
Files.copy(is, exePath);
134-
}
134+
135+
createResource(EXE_WRAPPER_NAME, params)
136+
.setCategory(I18N.getString("resource.installer-exe"))
137+
.setPublicName("installer.exe")
138+
.saveToFile(exePath);
135139

136140
new ExecutableRebrander().addAction((resourceLock) -> {
137141
// Embed msi in msi wrapper exe.

src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/WinResources.properties

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# Copyright (c) 2017, 2024, Oracle and/or its affiliates. All rights reserved.
2+
# Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved.
33
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
#
55
# This code is free software; you can redistribute it and/or modify it
@@ -41,6 +41,7 @@ resource.shortcutpromptdlg-wix-file=Shortcut prompt dialog WiX project file
4141
resource.installdirnotemptydlg-wix-file=Not empty install directory dialog WiX project file
4242
resource.launcher-as-service-wix-file=Service installer WiX project file
4343
resource.wix-src-conv=XSLT stylesheet converting WiX sources from WiX v3 to WiX v4 format
44+
resource.installer-exe=installer executable
4445

4546
error.no-wix-tools=Can not find WiX tools. Was looking for WiX v3 light.exe and candle.exe or WiX v4/v5 wix.exe and none was found
4647
error.no-wix-tools.advice=Download WiX 3.0 or later from https://wixtoolset.org and add it to the PATH.

test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherIconVerifier.java

Lines changed: 1 addition & 194 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,9 @@
2323

2424
package jdk.jpackage.test;
2525

26-
import java.awt.image.BufferedImage;
2726
import java.io.IOException;
28-
import java.lang.reflect.InvocationTargetException;
29-
import java.lang.reflect.Method;
3027
import java.nio.file.Files;
3128
import java.nio.file.Path;
32-
import java.util.Optional;
33-
import javax.imageio.ImageIO;
34-
import static jdk.jpackage.internal.util.function.ExceptionBox.rethrowUnchecked;
3529

3630
public final class LauncherIconVerifier {
3731
public LauncherIconVerifier() {
@@ -68,7 +62,7 @@ public void applyTo(JPackageCommand cmd) throws IOException {
6862

6963
if (TKit.isWindows()) {
7064
TKit.assertPathExists(iconPath, false);
71-
WinIconVerifier.instance.verifyLauncherIcon(cmd, launcherName,
65+
WinExecutableIconVerifier.verifyLauncherIcon(cmd, launcherName,
7266
expectedIcon, expectedDefault);
7367
} else if (expectedDefault) {
7468
TKit.assertPathExists(iconPath, true);
@@ -83,193 +77,6 @@ public void applyTo(JPackageCommand cmd) throws IOException {
8377
}
8478
}
8579

86-
private static class WinIconVerifier {
87-
88-
void verifyLauncherIcon(JPackageCommand cmd, String launcherName,
89-
Path expectedIcon, boolean expectedDefault) {
90-
TKit.withTempDirectory("icons", tmpDir -> {
91-
Path launcher = cmd.appLauncherPath(launcherName);
92-
Path iconWorkDir = tmpDir.resolve(launcher.getFileName());
93-
Path iconContainer = iconWorkDir.resolve("container.exe");
94-
Files.createDirectories(iconContainer.getParent());
95-
Files.copy(getDefaultAppLauncher(expectedIcon == null
96-
&& !expectedDefault), iconContainer);
97-
if (expectedIcon != null) {
98-
Executor.tryRunMultipleTimes(() -> {
99-
setIcon(expectedIcon, iconContainer);
100-
}, 3, 5);
101-
}
102-
103-
Path extractedExpectedIcon = extractIconFromExecutable(
104-
iconWorkDir, iconContainer, "expected");
105-
Path extractedActualIcon = extractIconFromExecutable(iconWorkDir,
106-
launcher, "actual");
107-
108-
TKit.trace(String.format(
109-
"Check icon file [%s] of %s launcher is a copy of source icon file [%s]",
110-
extractedActualIcon,
111-
Optional.ofNullable(launcherName).orElse("main"),
112-
extractedExpectedIcon));
113-
114-
if (Files.mismatch(extractedExpectedIcon, extractedActualIcon)
115-
!= -1) {
116-
// On Windows11 .NET API extracting icons from executables
117-
// produce slightly different output for the same icon.
118-
// To workaround it, compare pixels of images and if the
119-
// number of off pixels is below a threshold, assume
120-
// equality.
121-
BufferedImage expectedImg = ImageIO.read(
122-
extractedExpectedIcon.toFile());
123-
BufferedImage actualImg = ImageIO.read(
124-
extractedActualIcon.toFile());
125-
126-
int w = expectedImg.getWidth();
127-
int h = expectedImg.getHeight();
128-
129-
TKit.assertEquals(w, actualImg.getWidth(),
130-
"Check expected and actual icons have the same width");
131-
TKit.assertEquals(h, actualImg.getHeight(),
132-
"Check expected and actual icons have the same height");
133-
134-
int diffPixelCount = 0;
135-
136-
for (int i = 0; i != w; ++i) {
137-
for (int j = 0; j != h; ++j) {
138-
int expectedRGB = expectedImg.getRGB(i, j);
139-
int actualRGB = actualImg.getRGB(i, j);
140-
141-
if (expectedRGB != actualRGB) {
142-
TKit.trace(String.format(
143-
"Images mismatch at [%d, %d] pixel", i,
144-
j));
145-
diffPixelCount++;
146-
}
147-
}
148-
}
149-
150-
double threshold = 0.1;
151-
TKit.assertTrue(((double) diffPixelCount) / (w * h)
152-
< threshold,
153-
String.format(
154-
"Check the number of mismatched pixels [%d] of [%d] is < [%f] threshold",
155-
diffPixelCount, (w * h), threshold));
156-
}
157-
});
158-
}
159-
160-
private WinIconVerifier() {
161-
try {
162-
executableRebranderClass = Class.forName(
163-
"jdk.jpackage.internal.ExecutableRebrander");
164-
165-
lockResource = executableRebranderClass.getDeclaredMethod(
166-
"lockResource", String.class);
167-
// Note: this reflection call requires
168-
// --add-opens jdk.jpackage/jdk.jpackage.internal=ALL-UNNAMED
169-
lockResource.setAccessible(true);
170-
171-
unlockResource = executableRebranderClass.getDeclaredMethod(
172-
"unlockResource", long.class);
173-
unlockResource.setAccessible(true);
174-
175-
iconSwapWrapper = executableRebranderClass.getDeclaredMethod(
176-
"iconSwapWrapper", long.class, String.class);
177-
iconSwapWrapper.setAccessible(true);
178-
} catch (ClassNotFoundException | NoSuchMethodException
179-
| SecurityException ex) {
180-
throw rethrowUnchecked(ex);
181-
}
182-
}
183-
184-
private Path extractIconFromExecutable(Path outputDir, Path executable,
185-
String label) {
186-
// Run .NET code to extract icon from the given executable.
187-
// ExtractAssociatedIcon() will succeed even if the target file
188-
// is locked (by an antivirus). It will output a default icon
189-
// in case of error. To prevent this "fail safe" behavior we try
190-
// lock the target file with Open() call. If the attempt
191-
// fails ExtractAssociatedIcon() is not called and the script exits
192-
// with the exit code that will be trapped
193-
// inside of Executor.executeAndRepeatUntilExitCode() method that
194-
// will keep running the script until it succeeds or the number of
195-
// allowed attempts is exceeded.
196-
197-
Path extractedIcon = outputDir.resolve(label + ".bmp");
198-
String script = String.join(";",
199-
String.format(
200-
"try { [System.io.File]::Open('%s', 'Open', 'Read', 'None') } catch { exit 100 }",
201-
executable.toAbsolutePath().normalize()),
202-
"[System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')",
203-
String.format(
204-
"[System.Drawing.Icon]::ExtractAssociatedIcon('%s').ToBitmap().Save('%s', [System.Drawing.Imaging.ImageFormat]::Bmp)",
205-
executable.toAbsolutePath().normalize(),
206-
extractedIcon.toAbsolutePath().normalize()));
207-
208-
Executor.of("powershell", "-NoLogo", "-NoProfile", "-Command",
209-
script).executeAndRepeatUntilExitCode(0, 5, 10);
210-
211-
return extractedIcon;
212-
}
213-
214-
private Path getDefaultAppLauncher(boolean noIcon) {
215-
// Create app image with the sole purpose to get the default app launcher
216-
Path defaultAppOutputDir = TKit.workDir().resolve(String.format(
217-
"out-%d", ProcessHandle.current().pid()));
218-
JPackageCommand cmd = JPackageCommand.helloAppImage().setFakeRuntime().setArgumentValue(
219-
"--dest", defaultAppOutputDir);
220-
221-
String launcherName;
222-
if (noIcon) {
223-
launcherName = "no-icon";
224-
new AdditionalLauncher(launcherName).setNoIcon().applyTo(cmd);
225-
} else {
226-
launcherName = null;
227-
}
228-
229-
if (!Files.isExecutable(cmd.appLauncherPath(launcherName))) {
230-
cmd.execute();
231-
}
232-
return cmd.appLauncherPath(launcherName);
233-
}
234-
235-
private void setIcon(Path iconPath, Path launcherPath) {
236-
TKit.trace(String.format("Set icon of [%s] launcher to [%s] file",
237-
launcherPath, iconPath));
238-
try {
239-
launcherPath.toFile().setWritable(true, true);
240-
try {
241-
long lock = 0;
242-
try {
243-
lock = (Long) lockResource.invoke(null, new Object[]{
244-
launcherPath.toAbsolutePath().normalize().toString()});
245-
if (lock == 0) {
246-
throw new RuntimeException(String.format(
247-
"Failed to lock [%s] executable",
248-
launcherPath));
249-
}
250-
iconSwapWrapper.invoke(null, new Object[]{lock,
251-
iconPath.toAbsolutePath().normalize().toString()});
252-
} finally {
253-
if (lock != 0) {
254-
unlockResource.invoke(null, new Object[]{lock});
255-
}
256-
}
257-
} catch (IllegalAccessException | InvocationTargetException ex) {
258-
throw rethrowUnchecked(ex);
259-
}
260-
} finally {
261-
launcherPath.toFile().setWritable(false, true);
262-
}
263-
}
264-
265-
static final WinIconVerifier instance = new WinIconVerifier();
266-
267-
private final Class<?> executableRebranderClass;
268-
private final Method lockResource;
269-
private final Method unlockResource;
270-
private final Method iconSwapWrapper;
271-
}
272-
27380
private String launcherName;
27481
private Path expectedIcon;
27582
private boolean expectedDefault;

0 commit comments

Comments
 (0)