Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

8326447: jpackage creates Windows installers that cannot be signed #23732

Closed
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/jdk.jpackage/share/man/jpackage.md
Original file line number Diff line number Diff line change
@@ -686,6 +686,12 @@ jpackage will lookup files by specific names in the resource directory.

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

`installer.exe`

: Executable wrapper for MSI installer

Default resource is *msiwrapper.exe*


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

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2017, 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
@@ -24,6 +24,8 @@
*/
package jdk.jpackage.internal;

import static jdk.jpackage.internal.OverridableResource.createResource;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
@@ -129,9 +131,11 @@ private Path buildEXE(Map<String, ? super Object> params, Path msi,

// Copy template msi wrapper next to msi file
final Path exePath = PathUtils.replaceSuffix(msi, ".exe");
try (InputStream is = OverridableResource.readDefault(EXE_WRAPPER_NAME)) {
Files.copy(is, exePath);
}

createResource(EXE_WRAPPER_NAME, params)
.setCategory(I18N.getString("resource.installer-exe"))
.setPublicName("installer.exe")
.saveToFile(exePath);

new ExecutableRebrander().addAction((resourceLock) -> {
// Embed msi in msi wrapper exe.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# Copyright (c) 2017, 2024, Oracle and/or its affiliates. All rights reserved.
# Copyright (c) 2017, 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
@@ -41,6 +41,7 @@ resource.shortcutpromptdlg-wix-file=Shortcut prompt dialog WiX project file
resource.installdirnotemptydlg-wix-file=Not empty install directory dialog WiX project file
resource.launcher-as-service-wix-file=Service installer WiX project file
resource.wix-src-conv=XSLT stylesheet converting WiX sources from WiX v3 to WiX v4 format
resource.installer-exe=installer executable

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
error.no-wix-tools.advice=Download WiX 3.0 or later from https://wixtoolset.org and add it to the PATH.
Original file line number Diff line number Diff line change
@@ -23,15 +23,9 @@

package jdk.jpackage.test;

import java.awt.image.BufferedImage;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import javax.imageio.ImageIO;
import static jdk.jpackage.internal.util.function.ExceptionBox.rethrowUnchecked;

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

if (TKit.isWindows()) {
TKit.assertPathExists(iconPath, false);
WinIconVerifier.instance.verifyLauncherIcon(cmd, launcherName,
WinExecutableIconVerifier.verifyLauncherIcon(cmd, launcherName,
expectedIcon, expectedDefault);
} else if (expectedDefault) {
TKit.assertPathExists(iconPath, true);
@@ -83,193 +77,6 @@ public void applyTo(JPackageCommand cmd) throws IOException {
}
}

private static class WinIconVerifier {

void verifyLauncherIcon(JPackageCommand cmd, String launcherName,
Path expectedIcon, boolean expectedDefault) {
TKit.withTempDirectory("icons", tmpDir -> {
Path launcher = cmd.appLauncherPath(launcherName);
Path iconWorkDir = tmpDir.resolve(launcher.getFileName());
Path iconContainer = iconWorkDir.resolve("container.exe");
Files.createDirectories(iconContainer.getParent());
Files.copy(getDefaultAppLauncher(expectedIcon == null
&& !expectedDefault), iconContainer);
if (expectedIcon != null) {
Executor.tryRunMultipleTimes(() -> {
setIcon(expectedIcon, iconContainer);
}, 3, 5);
}

Path extractedExpectedIcon = extractIconFromExecutable(
iconWorkDir, iconContainer, "expected");
Path extractedActualIcon = extractIconFromExecutable(iconWorkDir,
launcher, "actual");

TKit.trace(String.format(
"Check icon file [%s] of %s launcher is a copy of source icon file [%s]",
extractedActualIcon,
Optional.ofNullable(launcherName).orElse("main"),
extractedExpectedIcon));

if (Files.mismatch(extractedExpectedIcon, extractedActualIcon)
!= -1) {
// On Windows11 .NET API extracting icons from executables
// produce slightly different output for the same icon.
// To workaround it, compare pixels of images and if the
// number of off pixels is below a threshold, assume
// equality.
BufferedImage expectedImg = ImageIO.read(
extractedExpectedIcon.toFile());
BufferedImage actualImg = ImageIO.read(
extractedActualIcon.toFile());

int w = expectedImg.getWidth();
int h = expectedImg.getHeight();

TKit.assertEquals(w, actualImg.getWidth(),
"Check expected and actual icons have the same width");
TKit.assertEquals(h, actualImg.getHeight(),
"Check expected and actual icons have the same height");

int diffPixelCount = 0;

for (int i = 0; i != w; ++i) {
for (int j = 0; j != h; ++j) {
int expectedRGB = expectedImg.getRGB(i, j);
int actualRGB = actualImg.getRGB(i, j);

if (expectedRGB != actualRGB) {
TKit.trace(String.format(
"Images mismatch at [%d, %d] pixel", i,
j));
diffPixelCount++;
}
}
}

double threshold = 0.1;
TKit.assertTrue(((double) diffPixelCount) / (w * h)
< threshold,
String.format(
"Check the number of mismatched pixels [%d] of [%d] is < [%f] threshold",
diffPixelCount, (w * h), threshold));
}
});
}

private WinIconVerifier() {
try {
executableRebranderClass = Class.forName(
"jdk.jpackage.internal.ExecutableRebrander");

lockResource = executableRebranderClass.getDeclaredMethod(
"lockResource", String.class);
// Note: this reflection call requires
// --add-opens jdk.jpackage/jdk.jpackage.internal=ALL-UNNAMED
lockResource.setAccessible(true);

unlockResource = executableRebranderClass.getDeclaredMethod(
"unlockResource", long.class);
unlockResource.setAccessible(true);

iconSwapWrapper = executableRebranderClass.getDeclaredMethod(
"iconSwapWrapper", long.class, String.class);
iconSwapWrapper.setAccessible(true);
} catch (ClassNotFoundException | NoSuchMethodException
| SecurityException ex) {
throw rethrowUnchecked(ex);
}
}

private Path extractIconFromExecutable(Path outputDir, Path executable,
String label) {
// Run .NET code to extract icon from the given executable.
// ExtractAssociatedIcon() will succeed even if the target file
// is locked (by an antivirus). It will output a default icon
// in case of error. To prevent this "fail safe" behavior we try
// lock the target file with Open() call. If the attempt
// fails ExtractAssociatedIcon() is not called and the script exits
// with the exit code that will be trapped
// inside of Executor.executeAndRepeatUntilExitCode() method that
// will keep running the script until it succeeds or the number of
// allowed attempts is exceeded.

Path extractedIcon = outputDir.resolve(label + ".bmp");
String script = String.join(";",
String.format(
"try { [System.io.File]::Open('%s', 'Open', 'Read', 'None') } catch { exit 100 }",
executable.toAbsolutePath().normalize()),
"[System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')",
String.format(
"[System.Drawing.Icon]::ExtractAssociatedIcon('%s').ToBitmap().Save('%s', [System.Drawing.Imaging.ImageFormat]::Bmp)",
executable.toAbsolutePath().normalize(),
extractedIcon.toAbsolutePath().normalize()));

Executor.of("powershell", "-NoLogo", "-NoProfile", "-Command",
script).executeAndRepeatUntilExitCode(0, 5, 10);

return extractedIcon;
}

private Path getDefaultAppLauncher(boolean noIcon) {
// Create app image with the sole purpose to get the default app launcher
Path defaultAppOutputDir = TKit.workDir().resolve(String.format(
"out-%d", ProcessHandle.current().pid()));
JPackageCommand cmd = JPackageCommand.helloAppImage().setFakeRuntime().setArgumentValue(
"--dest", defaultAppOutputDir);

String launcherName;
if (noIcon) {
launcherName = "no-icon";
new AdditionalLauncher(launcherName).setNoIcon().applyTo(cmd);
} else {
launcherName = null;
}

if (!Files.isExecutable(cmd.appLauncherPath(launcherName))) {
cmd.execute();
}
return cmd.appLauncherPath(launcherName);
}

private void setIcon(Path iconPath, Path launcherPath) {
TKit.trace(String.format("Set icon of [%s] launcher to [%s] file",
launcherPath, iconPath));
try {
launcherPath.toFile().setWritable(true, true);
try {
long lock = 0;
try {
lock = (Long) lockResource.invoke(null, new Object[]{
launcherPath.toAbsolutePath().normalize().toString()});
if (lock == 0) {
throw new RuntimeException(String.format(
"Failed to lock [%s] executable",
launcherPath));
}
iconSwapWrapper.invoke(null, new Object[]{lock,
iconPath.toAbsolutePath().normalize().toString()});
} finally {
if (lock != 0) {
unlockResource.invoke(null, new Object[]{lock});
}
}
} catch (IllegalAccessException | InvocationTargetException ex) {
throw rethrowUnchecked(ex);
}
} finally {
launcherPath.toFile().setWritable(false, true);
}
}

static final WinIconVerifier instance = new WinIconVerifier();

private final Class<?> executableRebranderClass;
private final Method lockResource;
private final Method unlockResource;
private final Method iconSwapWrapper;
}

private String launcherName;
private Path expectedIcon;
private boolean expectedDefault;
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
/*
* 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.test;

import static jdk.jpackage.internal.util.function.ExceptionBox.rethrowUnchecked;

import java.awt.image.BufferedImage;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import java.util.Optional;
import javax.imageio.ImageIO;

public final class WinExecutableIconVerifier {

static void verifyLauncherIcon(JPackageCommand cmd, String launcherName,
Path expectedIcon, boolean expectedDefault) {
Objects.requireNonNull(cmd);
INSTANCE.verifyExecutablesHaveSameIcon(new Input() {
@Override
public Path executableWithExpectedIcon(Path iconWorkDir) throws IOException {
final var iconContainer = iconWorkDir.resolve("container.exe");
Files.createDirectories(iconContainer.getParent());
Files.copy(INSTANCE.getDefaultAppLauncher(expectedIcon == null
&& !expectedDefault), iconContainer);
if (expectedIcon != null) {
Executor.tryRunMultipleTimes(() -> {
INSTANCE.setIcon(expectedIcon, iconContainer);
}, 3, 5);
}
return iconContainer;
}

@Override
public Path executableWithActualIcon(Path iconWorkDir) {
return cmd.appLauncherPath(launcherName);
}

@Override
public void trace(Path extractedActualIcon, Path extractedExpectedIcon) {
TKit.trace(String.format(
"Check icon file [%s] of %s launcher is a copy of source icon file [%s]",
extractedActualIcon,
Optional.ofNullable(launcherName).orElse("main"),
extractedExpectedIcon));
}
});
}

public static void verifyExecutablesHaveSameIcon(Path executableWithExpectedIcon,
Path executableWithActualIcon) {
Objects.requireNonNull(executableWithExpectedIcon);
Objects.requireNonNull(executableWithActualIcon);
INSTANCE.verifyExecutablesHaveSameIcon(new Input() {
@Override
public Path executableWithExpectedIcon(Path iconWorkDir) {
return executableWithExpectedIcon;
}

@Override
public Path executableWithActualIcon(Path iconWorkDir) {
return executableWithActualIcon;
}

@Override
public void trace(Path extractedActualIcon, Path extractedExpectedIcon) {
TKit.trace(String.format(
"Check icon file [%s] extracted from [%s] executable is a copy of icon file [%s] extracted from [%s] executable",
extractedActualIcon, executableWithActualIcon,
extractedExpectedIcon, executableWithExpectedIcon));
}
});
}

private interface Input {
Path executableWithExpectedIcon(Path iconWorkDir) throws IOException;
Path executableWithActualIcon(Path iconWorkDir) throws IOException;
void trace(Path extractedActualIcon, Path extractedExpectedIcon);
}

private void verifyExecutablesHaveSameIcon(Input input) {

Objects.requireNonNull(input);

TKit.withTempDirectory("icons", iconWorkDir -> {

final var executableWithExpectedIcon = input.executableWithExpectedIcon(iconWorkDir);
final var executableWithActualIcon = input.executableWithActualIcon(iconWorkDir);

if (Files.isSameFile(executableWithExpectedIcon, executableWithActualIcon)) {
throw new IllegalArgumentException("Supply different files for icon comparison");
}

Path extractedExpectedIcon = extractIconFromExecutable(
iconWorkDir, executableWithExpectedIcon, "expected");
Path extractedActualIcon = extractIconFromExecutable(iconWorkDir,
executableWithActualIcon, "actual");

input.trace(extractedActualIcon, extractedExpectedIcon);

// If executable doesn't have an icon, icon file will be empty.
// Both icon files must be empty or not empty.
// If only one icon file is empty executables have different icons.
final var expectedIconIsEmpty = isFileEmpty(extractedExpectedIcon);
final var actualIconIsEmpty = isFileEmpty(extractedActualIcon);

TKit.assertTrue(expectedIconIsEmpty == actualIconIsEmpty,
"Check both icon files are empty or not empty");

if (!expectedIconIsEmpty && Files.mismatch(extractedExpectedIcon, extractedActualIcon) != -1) {
// On Windows11 .NET API extracting icons from executables
// produce slightly different output for the same icon.
// To workaround it, compare pixels of images and if the
// number of off pixels is below a threshold, assume
// equality.
BufferedImage expectedImg = ImageIO.read(
extractedExpectedIcon.toFile());
BufferedImage actualImg = ImageIO.read(
extractedActualIcon.toFile());

int w = expectedImg.getWidth();
int h = expectedImg.getHeight();

TKit.assertEquals(w, actualImg.getWidth(),
"Check expected and actual icons have the same width");
TKit.assertEquals(h, actualImg.getHeight(),
"Check expected and actual icons have the same height");

int diffPixelCount = 0;

for (int i = 0; i != w; ++i) {
for (int j = 0; j != h; ++j) {
int expectedRGB = expectedImg.getRGB(i, j);
int actualRGB = actualImg.getRGB(i, j);

if (expectedRGB != actualRGB) {
TKit.trace(String.format(
"Images mismatch at [%d, %d] pixel", i,
j));
diffPixelCount++;
}
}
}

double threshold = 0.1;
TKit.assertTrue(((double) diffPixelCount) / (w * h)
< threshold,
String.format(
"Check the number of mismatched pixels [%d] of [%d] is < [%f] threshold",
diffPixelCount, (w * h), threshold));
}
});
}

private WinExecutableIconVerifier() {
try {
executableRebranderClass = Class.forName(
"jdk.jpackage.internal.ExecutableRebrander");

lockResource = executableRebranderClass.getDeclaredMethod(
"lockResource", String.class);
// Note: this reflection call requires
// --add-opens jdk.jpackage/jdk.jpackage.internal=ALL-UNNAMED
lockResource.setAccessible(true);

unlockResource = executableRebranderClass.getDeclaredMethod(
"unlockResource", long.class);
unlockResource.setAccessible(true);

iconSwap = executableRebranderClass.getDeclaredMethod(
"iconSwap", long.class, String.class);
iconSwap.setAccessible(true);
} catch (ClassNotFoundException | NoSuchMethodException
| SecurityException ex) {
throw rethrowUnchecked(ex);
}
}

private Path extractIconFromExecutable(Path outputDir, Path executable,
String extractedIconFilename) {

Objects.requireNonNull(outputDir);
Objects.requireNonNull(executable);
Objects.requireNonNull(extractedIconFilename);

Path extractedIcon = outputDir.resolve(extractedIconFilename + ".bmp");

Executor.of("powershell", "-NoLogo", "-NoProfile", "-ExecutionPolicy", "Unrestricted",
"-File", EXTRACT_ICON_PS1.toString(),
"-InputExecutable", executable.toAbsolutePath().normalize().toString(),
"-OutputIcon", extractedIcon.toAbsolutePath().normalize().toString()
).executeAndRepeatUntilExitCode(0, 5, 10);

return extractedIcon;
}

private Path getDefaultAppLauncher(boolean noIcon) {
// Create app image with the sole purpose to get the default app launcher
Path defaultAppOutputDir = TKit.workDir().resolve(String.format(
"out-%d", ProcessHandle.current().pid()));
JPackageCommand cmd = JPackageCommand.helloAppImage().setFakeRuntime().setArgumentValue(
"--dest", defaultAppOutputDir);

String launcherName;
if (noIcon) {
launcherName = "no-icon";
new AdditionalLauncher(launcherName).setNoIcon().applyTo(cmd);
} else {
launcherName = null;
}

if (!Files.isExecutable(cmd.appLauncherPath(launcherName))) {
cmd.execute();
}
return cmd.appLauncherPath(launcherName);
}

private void setIcon(Path iconPath, Path executable) {
Objects.requireNonNull(iconPath);
Objects.requireNonNull(executable);

try {
executable.toFile().setWritable(true, true);
try {
long lock = 0;
try {
lock = (Long) lockResource.invoke(null, new Object[]{
executable.toAbsolutePath().normalize().toString()});
if (lock == 0) {
throw new RuntimeException(String.format(
"Failed to lock [%s] executable",
executable));
}
var exitCode = (Integer) iconSwap.invoke(null, new Object[]{
lock,
iconPath.toAbsolutePath().normalize().toString()});
if (exitCode != 0) {
throw new RuntimeException(String.format(
"Failed to swap icon of [%s] executable",
executable));
}
} finally {
if (lock != 0) {
unlockResource.invoke(null, new Object[]{lock});
}
}
} catch (IllegalAccessException | InvocationTargetException ex) {
throw rethrowUnchecked(ex);
}
} finally {
executable.toFile().setWritable(false, true);
}
}

private static boolean isFileEmpty(Path file) {
return file.toFile().length() == 0;
}

private final Class<?> executableRebranderClass;
private final Method lockResource;
private final Method unlockResource;
private final Method iconSwap;

private static final WinExecutableIconVerifier INSTANCE = new WinExecutableIconVerifier();

private static final Path EXTRACT_ICON_PS1 = TKit.TEST_SRC_ROOT.resolve(Path.of("resources/read-executable-icon.ps1")).normalize();
}
54 changes: 54 additions & 0 deletions test/jdk/tools/jpackage/resources/read-executable-icon.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#
# 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.
#

param (
# Path to input executable file.
[Parameter(Mandatory=$true)]
[string]$InputExecutable,

# Path to BMP file where to save an icon extracted from the input executable.
[Parameter(Mandatory=$true)]
[string]$OutputIcon
)

Add-Type -AssemblyName 'System.Drawing'

$Shell32MethodDefinitions = @'
[DllImport("shell32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern uint ExtractIconEx(string szFileName, int nIconIndex, IntPtr[] phiconLarge, IntPtr[] phiconSmall, uint nIcons);
'@
$Shell32 = Add-Type -MemberDefinition $Shell32MethodDefinitions -Name 'Shell32' -Namespace 'Win32' -PassThru

$IconHandleArray = New-Object IntPtr[] 1 # Allocate IntPtr[1] to recieve HICON
$IconCount = $Shell32::ExtractIconEx($InputExecutable, 0, $IconHandleArray, $null, 1);
if ($IconCount -eq [uint32]::MaxValue) {
Write-Error "Failed to read icon."
exit 100
} elseif ($IconCount -ne 0) {
# Executable has an icon.
$Icon = [System.Drawing.Icon]::FromHandle($IconHandleArray[0]);
$Icon.ToBitmap().Save($OutputIcon, [System.Drawing.Imaging.ImageFormat]::Bmp)
} else {
# Execeutable doesn't have an icon. Empty output icon file.
$null = New-Item -Force $OutputIcon -ItemType File
}
102 changes: 102 additions & 0 deletions test/jdk/tools/jpackage/windows/WinInstallerResourceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright (c) 2021, 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.
*/

import static jdk.jpackage.test.WinExecutableIconVerifier.verifyExecutablesHaveSameIcon;

import java.nio.file.Files;
import java.nio.file.Path;
import jdk.jpackage.test.Annotations.Test;
import jdk.jpackage.test.JPackageCommand;
import jdk.jpackage.test.PackageTest;
import jdk.jpackage.test.PackageType;
import jdk.jpackage.test.RunnablePackageTest.Action;
import jdk.jpackage.test.TKit;

/**
* Test for installer exe from the resource directory.
*/

/*
* @test
* @summary jpackage with installer exe from the resource directory
* @library /test/jdk/tools/jpackage/helpers
* @key jpackagePlatformPackage
* @build jdk.jpackage.test.*
* @compile -Xlint:all -Werror WinInstallerResourceTest.java
* @requires (os.family == "windows")
* @run main/othervm/timeout=720 -Xmx512m jdk.jpackage.test.Main
* --jpt-run=WinInstallerResourceTest
*/
public class WinInstallerResourceTest {

@Test
public void test() {
createPackageTest("dummy")
.addInitializer(JPackageCommand::setFakeRuntime)
.addInitializer(cmd -> {
// Create exe installer using the default installer exe resource and a custom icon.
cmd.addArguments("--icon", iconPath("icon"));
})
.addBundleVerifier(cmd -> {
final var exeInstaller = cmd.outputBundle();

createPackageTest("InstallerResTest")
.addInitializer(cmd2 -> {
cmd2.setArgumentValue("--runtime-image", cmd.getArgumentValue("--runtime-image"));
})
.addInitializer(cmd2 -> {
//
// Create an exe installer using the exe installer created in the first jpackage run.
//

// The exe installer created in the first jpackage run has a custom icon.
// Configure the second jpackage run to use the default icon.
// This will prevent jpackage from editing icon in the exe installer.
// Copy the exe installer created in the first jpackage run into the
// resource directory for the second jpackage run.
// If jpackage will pick an exe installer resource from the resource directory,
// the output exe installer should have the same icon as
// the exe installer produced in the first jpackage run.
final var resourceDir = TKit.createTempDirectory("resources");
Files.copy(exeInstaller, resourceDir.resolve("installer.exe"));
cmd2.addArguments("--resource-dir", resourceDir);
})
.addBundleVerifier(cmd2 -> {
verifyExecutablesHaveSameIcon(exeInstaller, cmd2.outputBundle());
}).run(Action.CREATE);
}).run(Action.CREATE);
}

private PackageTest createPackageTest(String name) {
return new PackageTest()
.ignoreBundleOutputDir()
.forTypes(PackageType.WIN_EXE)
.configureHelloApp()
.addInitializer(cmd -> cmd.setArgumentValue("--name", name));
}

private static Path iconPath(String name) {
return TKit.TEST_SRC_ROOT.resolve(Path.of("resources", name
+ TKit.ICON_SUFFIX));
}
}