8150442: Enforce Supported Platforms in Packager for MSI bundles

Reviewed-by: almatvee, cstein
This commit is contained in:
Alexey Semenyuk 2025-02-11 19:22:35 +00:00
parent 642816538f
commit e7157d174c
14 changed files with 404 additions and 10 deletions

View file

@ -29,7 +29,7 @@ DISABLED_WARNINGS_java += dangling-doc-comments
COPY += .gif .png .txt .spec .script .prerm .preinst \
.postrm .postinst .list .sh .desktop .copyright .control .plist .template \
.icns .scpt .wxs .wxl .wxi .ico .bmp .tiff .service .xsl
.icns .scpt .wxs .wxl .wxi .wxf .ico .bmp .tiff .service .xsl
CLEAN += .properties

View file

@ -1,5 +1,5 @@
---
# Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved.
# Copyright (c) 2018, 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
@ -661,6 +661,12 @@ jpackage will lookup files by specific names in the resource directory.
: WiX project file for installer UI
`os-condition.wxf`
: WiX project file with the condition to block installation on older versions of Windows
Default resource is *os-condition.wxf*
`wix-conv.xsl`
: WiX source code converter. Used for converting WiX sources from WiX v3 to v4 schema when WiX v4 or newer is used

View file

@ -0,0 +1,205 @@
/*
* 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.WinMsiBundler.WIN_APP_IMAGE;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import jdk.jpackage.internal.util.XmlConsumer;
/**
* WiX Condition to block/allow installation based on OS version.
*/
record OSVersionCondition(WindowsVersion version) {
static OSVersionCondition createFromAppImage(ApplicationLayout appLayout, Map<String, ? super Object> params) {
Objects.requireNonNull(appLayout);
final List<Path> executables = new ArrayList<>();
if (!StandardBundlerParam.isRuntimeInstaller(params)) {
final var launcherName = StandardBundlerParam.APP_NAME.fetchFrom(params);
executables.add(appLayout.launchersDirectory().resolve(launcherName + ".exe"));
}
executables.add(appLayout.runtimeDirectory().resolve("bin\\java.dll"));
final var lowestOsVersion = executables.stream()
.filter(Files::isRegularFile)
.map(WindowsVersion::getExecutableOSVersion)
// Order by version, with the higher version first
.sorted(WindowsVersion.descendingOrder())
.findFirst().orElseGet(() -> {
// No java.dll, no launchers, it is either a highly customized or messed up app image.
// Let it install on Windows NT/95 or newer.
return new WindowsVersion(4, 0);
});
return new OSVersionCondition(lowestOsVersion);
}
record WindowsVersion(int majorOSVersion, int minorOSVersion) {
WindowsVersion {
if (majorOSVersion <= 0) {
throw new IllegalArgumentException("Invalid major version");
}
if (minorOSVersion < 0) {
throw new IllegalArgumentException("Invalid minor version");
}
}
static WindowsVersion getExecutableOSVersion(Path executable) {
try (final var fin = Files.newInputStream(executable);
final var in = new BufferedInputStream(fin)) {
// Skip all but "e_lfanew" fields of DOS stub (https://wiki.osdev.org/PE#DOS_Stub)
in.skipNBytes(64 - 4);
final int peHeaderOffset = read32BitLE(in);
if (peHeaderOffset <= 0) {
throw new IOException("Invalid PE header offset");
}
// Move to PE header
in.skip(peHeaderOffset - 64);
// Read "mMagic" field (aka PE signature), (https://wiki.osdev.org/PE#PE_header)
final byte[] peSignature = in.readNBytes(4);
if (peSignature.length != 4) {
throw notEnoughBytes();
}
if (peSignature[0] != 'P' || peSignature[1] != 'E' || peSignature[2] != 0 || peSignature[3] != 0) {
throw new IOException(String.format("Invalid PE signature: %s", HexFormat.of().formatHex(peSignature)));
}
// Read size of optional PE header from "mSizeOfOptionalHeader" field (https://wiki.osdev.org/PE#PE_header)
in.skip(16);
final int sizeOfOptionalHeader = read16BitLE(in);
if (sizeOfOptionalHeader < (40 + 4)) {
throw new IOException("Invalid PE optional header size");
}
// Skip PE header
in.skip(2);
// Skip all fields of Optional PE header until "mMajorOperatingSystemVersion" field (https://wiki.osdev.org/PE#Optional_header)
in.skip(40);
final int mMajorOperatingSystemVersion = read16BitLE(in);
final int mMinorOperatingSystemVersion = read16BitLE(in);
return new WindowsVersion(mMajorOperatingSystemVersion, mMinorOperatingSystemVersion);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
static Comparator<WindowsVersion> descendingOrder() {
return Comparator.comparing(WindowsVersion::majorOSVersion).thenComparing(WindowsVersion::minorOSVersion).reversed();
}
private static int read16BitLE(InputStream in) throws IOException {
byte buffer[] = new byte[2];
if (buffer.length != in.read(buffer)) {
throw notEnoughBytes();
}
return ((buffer[0] & 0xFF) | ((buffer[1] & 0xFF) << 8));
}
private static int read32BitLE(InputStream in) throws IOException {
byte buffer[] = new byte[4];
if (buffer.length != in.read(buffer)) {
throw notEnoughBytes();
}
return ((buffer[0] & 0xFF) | ((buffer[1] & 0xFF) << 8) |
((buffer[2] & 0xFF) << 16) | ((buffer[3] & 0xFF) << 24));
}
private static IOException notEnoughBytes() {
return new IOException("Invalid PE file");
}
}
int msiVersionNumber() {
return version.majorOSVersion() * 100 + version.minorOSVersion();
}
String msiVersionString() {
return String.valueOf(msiVersionNumber());
}
static WixFragmentBuilder createWixFragmentBuilder() {
final var builder = new WixFragmentBuilder() {
@Override
protected Collection<XmlConsumer> getFragmentWriters() {
return Collections.emptyList();
}
@Override
void initFromParams(Map<String, ? super Object> params) {
super.initFromParams(params);
final Path appImageRoot = WIN_APP_IMAGE.fetchFrom(params);
ApplicationLayout appImageLayout;
if (StandardBundlerParam.isRuntimeInstaller(params)) {
appImageLayout = ApplicationLayout.javaRuntime();
} else {
appImageLayout = ApplicationLayout.platformAppImage();
}
appImageLayout = appImageLayout.resolveAt(appImageRoot);
final var cond = OSVersionCondition.createFromAppImage(appImageLayout, params);
setWixVariable("JpExecutableMajorOSVersion", String.valueOf(cond.version().majorOSVersion));
setWixVariable("JpExecutableMinorOSVersion", String.valueOf(cond.version().minorOSVersion));
setWixVariable("JpExecutableOSVersion", String.valueOf(cond.msiVersionString()));
}
};
builder.setDefaultResourceName("os-condition.wxf");
return builder;
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved.
* 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
@ -227,7 +227,8 @@ public class WinMsiBundler extends AbstractBundler {
appImageBundler = new WinAppBundler().setDependentTask(true);
wixFragments = Stream.of(
Map.entry("bundle.wxf", new WixAppImageFragmentBuilder()),
Map.entry("ui.wxf", new WixUiFragmentBuilder())
Map.entry("ui.wxf", new WixUiFragmentBuilder()),
Map.entry("os-condition.wxf", OSVersionCondition.createWixFragmentBuilder())
).<WixFragmentBuilder>map(e -> {
e.getValue().setOutputFileName(e.getKey());
return e.getValue();

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved.
* 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
@ -38,7 +38,6 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.stream.XMLStreamWriter;
import jdk.jpackage.internal.util.XmlConsumer;
import jdk.jpackage.internal.OverridableResource.Source;
import static jdk.jpackage.internal.StandardBundlerParam.CONFIG_ROOT;
import jdk.internal.util.Architecture;
import static jdk.jpackage.internal.OverridableResource.createResource;
@ -62,12 +61,15 @@ abstract class WixFragmentBuilder {
outputFileName = v;
}
final void setDefaultResourceName(String v) {
defaultResourceName = v;
}
void initFromParams(Map<String, ? super Object> params) {
wixVariables = null;
additionalResources = null;
configRoot = CONFIG_ROOT.fetchFrom(params);
fragmentResource = createResource(outputFileName, params).setSourceOrder(
Source.ResourceDir);
fragmentResource = createResource(defaultResourceName, params).setPublicName(outputFileName);
}
List<String> getLoggableWixFeatures() {
@ -82,7 +84,10 @@ abstract class WixFragmentBuilder {
void addFilesToConfigRoot() throws IOException {
Path fragmentPath = configRoot.resolve(outputFileName);
if (fragmentResource.saveToFile(fragmentPath) == null) {
final var src = fragmentResource.saveToStream(null);
if (src == null) {
// There is no predefined resource for the fragment.
// The fragment should be built in the format matching the version of the WiX Toolkit.
createWixSource(fragmentPath, xml -> {
for (var fragmentWriter : getFragmentWriters()) {
xml.writeStartElement("Fragment");
@ -90,6 +95,11 @@ abstract class WixFragmentBuilder {
xml.writeEndElement(); // <Fragment>
}
});
} else {
// Fragment is picked from the resource. May require conversion.
final var resourceGroup = new ResourceGroup(getWixType());
resourceGroup.addResource(fragmentResource, fragmentPath);
resourceGroup.saveResources();
}
if (additionalResources != null) {
@ -237,6 +247,7 @@ abstract class WixFragmentBuilder {
private WixVariables wixVariables;
private ResourceGroup additionalResources;
private OverridableResource fragmentResource;
private String defaultResourceName;
private String outputFileName;
private Path configRoot;
}

View file

@ -15,4 +15,6 @@
<String Id="InstallDirNotEmptyDlgInstallDirExistMessage">Der Ordner [INSTALLDIR] ist bereits vorhanden. Möchten Sie diesen Ordner trotzdem installieren?</String>
<String Id="ContextMenuCommandLabel">Mit [ProductName] öffnen</String>
<String Id="OsConditionMessage">[ProductName][ProductVersion] is not supported on this version of Windows</String>
</WixLocalization>

View file

@ -15,4 +15,6 @@
<String Id="InstallDirNotEmptyDlgInstallDirExistMessage">The folder [INSTALLDIR] already exist. Would you like to install to that folder anyway?</String>
<String Id="ContextMenuCommandLabel">Open with [ProductName]</String>
<String Id="OsConditionMessage">[ProductName][ProductVersion] is not supported on this version of Windows</String>
</WixLocalization>

View file

@ -15,4 +15,6 @@
<String Id="InstallDirNotEmptyDlgInstallDirExistMessage">フォルダ[INSTALLDIR]はすでに存在します。そのフォルダにインストールしますか?</String>
<String Id="ContextMenuCommandLabel">[ProductName]で開く</String>
<String Id="OsConditionMessage">[ProductName][ProductVersion] is not supported on this version of Windows</String>
</WixLocalization>

View file

@ -15,4 +15,6 @@
<String Id="InstallDirNotEmptyDlgInstallDirExistMessage">文件夹 [INSTALLDIR] 已存在。是否仍要安装到该文件夹?</String>
<String Id="ContextMenuCommandLabel">使用 [ProductName] 打开</String>
<String Id="OsConditionMessage">[ProductName][ProductVersion] is not supported on this version of Windows</String>
</WixLocalization>

View file

@ -76,6 +76,7 @@
<ComponentGroupRef Id="Shortcuts"/>
<ComponentGroupRef Id="Files"/>
<ComponentGroupRef Id="FileAssociations"/>
<ComponentGroupRef Id="FragmentOsCondition"/>
</Feature>
<CustomAction Id="JpSetARPINSTALLLOCATION" Property="ARPINSTALLLOCATION" Value="[INSTALLDIR]" />
@ -123,6 +124,7 @@
<?ifndef JpAllowDowngrades ?>
<Custom Action="JpDisallowDowngrade" After="JpFindRelatedProducts">JP_DOWNGRADABLE_FOUND</Custom>
<?endif?>
<LaunchConditions Before="AppSearch"/>
<RemoveExistingProducts Before="CostInitialize"/>
<Custom Action="JpFindRelatedProducts" After="FindRelatedProducts"/>
</InstallExecuteSequence>

View file

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
* 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.
*/
-->
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Fragment>
<Condition Message="!(loc.OsConditionMessage)">
<![CDATA[Installed OR (VersionNT >= $(var.JpExecutableOSVersion))]]>
</Condition>
<!--
Fragment contents must be referenced. Otherwise, the fragment will be ignored.
Use empty component group as an anchor for the reference.
-->
<ComponentGroup Id="FragmentOsCondition"/>
</Fragment>
</Wix>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2024, 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
@ -73,6 +73,19 @@
</xsl:template>
<!--
From <Condition Message="foo">Bar</Condition> to <Launch Message="foo" Condition="Bar"/>
-->
<xsl:template match="wix3:Condition">
<xsl:element name="Launch" namespace="http://wixtoolset.org/schemas/v4/wxs">
<xsl:attribute name="Condition">
<xsl:value-of select="text()"/>
</xsl:attribute>
<xsl:apply-templates select="@*"/>
</xsl:element>
</xsl:template>
<!--
Wix3 Product (https://wixtoolset.org/docs/v3/xsd/wix/product/):
Id

View file

@ -0,0 +1,79 @@
/*
* 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;
import static jdk.jpackage.internal.OSVersionCondition.WindowsVersion.getExecutableOSVersion;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.condition.OS.WINDOWS;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;
import jdk.jpackage.internal.OSVersionCondition.WindowsVersion;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
public class ExecutableOSVersionTest {
@Test
@EnabledOnOs(WINDOWS)
public void testWindowsVersionGetExecutableOSVersion() {
final var javaHome = Path.of(System.getProperty("java.home"));
final var javaExeVer = getExecutableOSVersion(javaHome.resolve("bin/java.exe"));
assertTrue(javaExeVer.majorOSVersion() > 0);
assertTrue(javaExeVer.minorOSVersion() >= 0);
final var javaDllVer = getExecutableOSVersion(javaHome.resolve("bin/java.dll"));
assertEquals(javaExeVer, javaDllVer);
}
@ParameterizedTest
@EnabledOnOs(WINDOWS)
@MethodSource
public void testWindowsVersionDescendingOrder(List<WindowsVersion> unsorted, WindowsVersion expectedFirst) {
final var actualFirst = unsorted.stream().sorted(WindowsVersion.descendingOrder()).findFirst().orElseThrow();
assertEquals(expectedFirst, actualFirst);
}
public static Stream<Object[]> testWindowsVersionDescendingOrder() {
return Stream.<Object[]>of(
new Object[] { List.of(wver(5, 0), wver(5, 1), wver(4, 9)), wver(5, 1) },
new Object[] { List.of(wver(5, 0)), wver(5, 0) },
new Object[] { List.of(wver(5, 1), wver(5, 1), wver(5, 0)), wver(5, 1) },
new Object[] { List.of(wver(3, 11), wver(4, 8), wver(5, 6)), wver(5, 6) },
new Object[] { List.of(wver(3, 11), wver(3, 9), wver(3, 13)), wver(3, 13) }
);
}
private final static WindowsVersion wver(int major, int minor) {
return new WindowsVersion(major, minor);
}
}

View file

@ -0,0 +1,29 @@
/*
* 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.
*/
/* @test
* @summary Test function reading OS version from PE file
* @requires (os.family == "windows")
* @compile/module=jdk.jpackage jdk/jpackage/internal/ExecutableOSVersionTest.java
* @run junit jdk.jpackage/jdk.jpackage.internal.ExecutableOSVersionTest
*/