diff --git a/make/modules/jdk.jpackage/Java.gmk b/make/modules/jdk.jpackage/Java.gmk index 1b6db5fab05..da66fc14009 100644 --- a/make/modules/jdk.jpackage/Java.gmk +++ b/make/modules/jdk.jpackage/Java.gmk @@ -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 diff --git a/src/jdk.jpackage/share/man/jpackage.md b/src/jdk.jpackage/share/man/jpackage.md index faaae6acfbb..edc76f7caff 100644 --- a/src/jdk.jpackage/share/man/jpackage.md +++ b/src/jdk.jpackage/share/man/jpackage.md @@ -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 diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/OSVersionCondition.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/OSVersionCondition.java new file mode 100644 index 00000000000..80bb0c77625 --- /dev/null +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/OSVersionCondition.java @@ -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 params) { + Objects.requireNonNull(appLayout); + + final List 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 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 getFragmentWriters() { + return Collections.emptyList(); + } + + @Override + void initFromParams(Map 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; + } +} diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java index 1c96b5885e7..3b2e7569db3 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinMsiBundler.java @@ -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()) ).map(e -> { e.getValue().setOutputFileName(e.getKey()); return e.getValue(); diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixFragmentBuilder.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixFragmentBuilder.java index 48a1d04f8dc..6d9f008dd7d 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixFragmentBuilder.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixFragmentBuilder.java @@ -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 params) { wixVariables = null; additionalResources = null; configRoot = CONFIG_ROOT.fetchFrom(params); - fragmentResource = createResource(outputFileName, params).setSourceOrder( - Source.ResourceDir); + fragmentResource = createResource(defaultResourceName, params).setPublicName(outputFileName); } List 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(); // } }); + } 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; } diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/MsiInstallerStrings_de.wxl b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/MsiInstallerStrings_de.wxl index 31be69aa5d0..5e30d3744ec 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/MsiInstallerStrings_de.wxl +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/MsiInstallerStrings_de.wxl @@ -15,4 +15,6 @@ Der Ordner [INSTALLDIR] ist bereits vorhanden. Möchten Sie diesen Ordner trotzdem installieren? Mit [ProductName] öffnen + + [ProductName][ProductVersion] is not supported on this version of Windows diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/MsiInstallerStrings_en.wxl b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/MsiInstallerStrings_en.wxl index 070e46621fb..6f0482549ea 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/MsiInstallerStrings_en.wxl +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/MsiInstallerStrings_en.wxl @@ -15,4 +15,6 @@ The folder [INSTALLDIR] already exist. Would you like to install to that folder anyway? Open with [ProductName] + + [ProductName][ProductVersion] is not supported on this version of Windows diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/MsiInstallerStrings_ja.wxl b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/MsiInstallerStrings_ja.wxl index 32a57433829..4639e2f8fed 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/MsiInstallerStrings_ja.wxl +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/MsiInstallerStrings_ja.wxl @@ -15,4 +15,6 @@ フォルダ[INSTALLDIR]はすでに存在します。そのフォルダにインストールしますか? [ProductName]で開く + + [ProductName][ProductVersion] is not supported on this version of Windows diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/MsiInstallerStrings_zh_CN.wxl b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/MsiInstallerStrings_zh_CN.wxl index 978f74a1546..06974dd84e5 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/MsiInstallerStrings_zh_CN.wxl +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/MsiInstallerStrings_zh_CN.wxl @@ -15,4 +15,6 @@ 文件夹 [INSTALLDIR] 已存在。是否仍要安装到该文件夹? 使用 [ProductName] 打开 + + [ProductName][ProductVersion] is not supported on this version of Windows diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/main.wxs b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/main.wxs index b884e0829be..2a3ea3743e2 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/main.wxs +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/main.wxs @@ -76,6 +76,7 @@ + @@ -123,6 +124,7 @@ JP_DOWNGRADABLE_FOUND + diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/os-condition.wxf b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/os-condition.wxf new file mode 100644 index 00000000000..338f9ac76aa --- /dev/null +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/os-condition.wxf @@ -0,0 +1,40 @@ + + + + + + + = $(var.JpExecutableOSVersion))]]> + + + + + diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/wix3-to-wix4-conv.xsl b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/wix3-to-wix4-conv.xsl index 382ed731b5a..1122989d062 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/wix3-to-wix4-conv.xsl +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/wix3-to-wix4-conv.xsl @@ -1,7 +1,7 @@ + + + + + + + + + +