diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/util/PListWriter.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/util/PListWriter.java index 381faee3802e3..c23c292b8a8de 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/util/PListWriter.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/util/PListWriter.java @@ -90,7 +90,7 @@ public static void writeArray(XMLStreamWriter xml, XmlConsumer content) public static void writePList(XMLStreamWriter xml, XmlConsumer content) throws XMLStreamException, IOException { - xml.writeDTD("plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"https://www.apple.com/DTDs/PropertyList-1.0.dtd\""); + xml.writeDTD(""); xml.writeStartElement("plist"); xml.writeAttribute("version", "1.0"); content.accept(xml); diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java index 6aacc261fb64c..19c76cc7bbe75 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java @@ -363,6 +363,29 @@ public static JPackageCommand helloAppImage(JavaAppDesc javaAppDesc) { return cmd; } + public static Path createInputRuntimeImage() throws IOException { + + final Path runtimeImageDir; + + if (JPackageCommand.DEFAULT_RUNTIME_IMAGE != null) { + runtimeImageDir = JPackageCommand.DEFAULT_RUNTIME_IMAGE; + } else { + runtimeImageDir = TKit.createTempDirectory("runtime-image").resolve("data"); + + new Executor().setToolProvider(JavaTool.JLINK) + .dumpOutput() + .addArguments( + "--output", runtimeImageDir.toString(), + "--add-modules", "java.desktop", + "--strip-debug", + "--no-header-files", + "--no-man-pages") + .execute(); + } + + return runtimeImageDir; + } + public JPackageCommand setPackageType(PackageType type) { verifyMutable(); type.applyTo(this); diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java index 37e395e1a3eca..241e2f130be74 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java @@ -143,6 +143,10 @@ public static PListReader readPListFromAppImage(Path appImage) { return readPList(appImage.resolve("Contents/Info.plist")); } + public static PListReader readPListFromEmbeddedRuntime(Path appImage) { + return readPList(appImage.resolve("Contents/runtime/Contents/Info.plist")); + } + public static PListReader readPList(Path path) { TKit.assertReadableFileExists(path); return ThrowingSupplier.toSupplier(() -> readPList(Files.readAllLines( diff --git a/test/jdk/tools/jpackage/macosx/CustomInfoPListTest.java b/test/jdk/tools/jpackage/macosx/CustomInfoPListTest.java new file mode 100644 index 0000000000000..c617b4150dee4 --- /dev/null +++ b/test/jdk/tools/jpackage/macosx/CustomInfoPListTest.java @@ -0,0 +1,257 @@ +/* + * 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 --resource-dir with custom "Info.plist" for the top-level bundle + * and "Runtime-Info.plist" for the embedded runtime bundle + */ + +/* + * @test + * @summary jpackage with --type image --resource-dir "Info.plist" and "Runtime-Info.plist" + * @library /test/jdk/tools/jpackage/helpers + * @key jpackagePlatformPackage + * @build jdk.jpackage.test.* + * @build CustomInfoPListTest + * @requires (os.family == "mac") + * @requires (jpackage.test.SQETest == null) + * @run main/othervm/timeout=1440 -Xmx512m jdk.jpackage.test.Main + * --jpt-run=CustomInfoPListTest + */ + +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.MacHelper; +import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.JPackageStringBundle; +import jdk.jpackage.test.PackageType; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.concurrent.Executor; + +import javax.xml.stream.XMLOutputFactory; + +import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.Annotations.Parameter; + +import jdk.jpackage.internal.util.XmlUtils; +import jdk.jpackage.internal.util.PListReader; + +import static jdk.jpackage.internal.util.PListWriter.writePList; +import static jdk.jpackage.internal.util.PListWriter.writeDict; +import static jdk.jpackage.internal.util.PListWriter.writeString; +import static jdk.jpackage.internal.util.XmlUtils.toXmlConsumer; + +public class CustomInfoPListTest { + + private static final String APP_PLIST_KEY = "CustomAppPList"; + private static final String EMBEDDED_RUNTIME_PLIST_KEY = "CustomEmbeddedRuntimePList"; + private static final String RUNTIME_PLIST_KEY = "CustomRuntimePList"; + + private static final Map appKeyValue = new HashMap<>(); + private static final Map embeddedRuntimeKeyValue = new HashMap<>(); + private static final Map runtimeKeyValue = new HashMap<>(); + + private static final List faKeysAndValues = new ArrayList<>(); + + static { + appKeyValue.put("CFBundleExecutable", "AppCustomInfoPListTest"); + appKeyValue.put("CFBundleIconFile", "AppCustomInfoPListTest.icns"); + appKeyValue.put("CFBundleIdentifier", "Hello"); + appKeyValue.put("CFBundleName", "AppCustomInfoPListTest"); + appKeyValue.put("CFBundleShortVersionString", "1.0"); + appKeyValue.put("LSApplicationCategoryType", "public.app-category.utilities"); + appKeyValue.put("CFBundleVersion", "1.0"); + appKeyValue.put("NSHumanReadableCopyright", JPackageStringBundle.MAIN.cannedFormattedString( + "param.copyright.default", new Date()).getValue()); + + embeddedRuntimeKeyValue.put("CFBundleIdentifier", "Hello"); + embeddedRuntimeKeyValue.put("CFBundleName", "AppCustomInfoPListTest"); + embeddedRuntimeKeyValue.put("CFBundleShortVersionString", "1.0"); + embeddedRuntimeKeyValue.put("CFBundleVersion", "1.0"); + + runtimeKeyValue.put("CFBundleIdentifier", "foo"); + runtimeKeyValue.put("CFBundleName", "foo"); + runtimeKeyValue.put("CFBundleShortVersionString", "1.0"); + runtimeKeyValue.put("CFBundleVersion", "1.0"); + + faKeysAndValues.add("CFBundleDocumentTypes"); + faKeysAndValues.add("LSItemContentTypes"); + faKeysAndValues.add("Hello.foo"); + faKeysAndValues.add("UTTypeDescription"); + faKeysAndValues.add("CFBundleTypeName"); + faKeysAndValues.add("bar"); + } + + // We do not need full and valid Info.plist for testing + private static void createInfoPListFile(String key, Path plistFile) { + try { + XmlUtils.createXml(plistFile, xml -> { + writePList(xml, toXmlConsumer(() -> { + writeDict(xml, toXmlConsumer(() -> { + writeString(xml, "CustomInfoPListTestKey", key); + if (key.equals(APP_PLIST_KEY)) { + // Application + writeString(xml, "CFBundleExecutable", "DEPLOY_LAUNCHER_NAME"); + writeString(xml, "CFBundleIconFile", "DEPLOY_ICON_FILE"); + writeString(xml, "CFBundleIdentifier", "DEPLOY_BUNDLE_IDENTIFIER"); + writeString(xml, "CFBundleName", "DEPLOY_BUNDLE_NAME"); + writeString(xml, "CFBundleShortVersionString", "DEPLOY_BUNDLE_SHORT_VERSION"); + writeString(xml, "LSApplicationCategoryType", "DEPLOY_APP_CATEGORY"); + writeString(xml, "CFBundleVersion", "DEPLOY_BUNDLE_CFBUNDLE_VERSION"); + writeString(xml, "NSHumanReadableCopyright", "DEPLOY_BUNDLE_COPYRIGHT"); + writeString(xml, "CustomInfoPListFA", "DEPLOY_FILE_ASSOCIATIONS"); + } else if (key.equals(EMBEDDED_RUNTIME_PLIST_KEY) || key.equals(RUNTIME_PLIST_KEY)) { + // Embedded runtime and runtime + writeString(xml, "CFBundleIdentifier", "CF_BUNDLE_IDENTIFIER"); + writeString(xml, "CFBundleName", "CF_BUNDLE_NAME"); + writeString(xml, "CFBundleShortVersionString", "CF_BUNDLE_SHORT_VERSION_STRING"); + writeString(xml, "CFBundleVersion", "CF_BUNDLE_VERSION"); + } + })); + })); + }); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private static String getResourceDirWithCustomInfoPList( + String key, boolean includeMainPList, boolean includeRuntimePList) { + final Path resources = TKit.createTempDirectory("resources"); + if (includeMainPList) { + createInfoPListFile(key, resources.resolve("Info.plist")); + } + if (includeRuntimePList) { + createInfoPListFile(EMBEDDED_RUNTIME_PLIST_KEY, resources.resolve("Runtime-Info.plist")); + } + return resources.toString(); + } + + private static void validateInfoPListFileKey(PListReader plistFile, Optional key) { + if (key.isPresent()) { + TKit.assertEquals(key.get(), plistFile.queryValue("CustomInfoPListTestKey"), String.format( + "Check value of %s plist key", "CustomInfoPListTestKey")); + } else { + boolean exceptionThrown = false; + try { + plistFile.queryValue("CustomInfoPListTestKey"); + } catch (NoSuchElementException ex) { + exceptionThrown = true; + } + TKit.assertTrue(exceptionThrown, "NoSuchElementException exception not thrown"); + } + } + + private static void validateInfoPList(PListReader plistFile, Map values) { + values.forEach((key, value) -> { + TKit.assertEquals(value, plistFile.queryValue(key), String.format( + "Check value of %s plist key", key)); + }); + } + + // For FA check that main keys and values are present + private static void validateInfoPListFA(PListReader plistFile, List values) { + String faXml = plistFile.queryValue("CustomInfoPListFA"); + values.forEach(value -> { + TKit.assertTrue(faXml.contains(value), String.format( + "Check FA key/value is present [%s]", value)); + }); + } + + @Test + @Parameter({"TRUE", "FALSE"}) + @Parameter({"FALSE", "TRUE"}) + @Parameter({"TRUE", "TRUE"}) + public void testApp(boolean includeMainPList, boolean includeRuntimePList) { + final Path propFile = TKit.workDir().resolve("fa.properties"); + TKit.createPropertiesFile(propFile, Map.of( + "mime-type", "application/x-jpackage-foo", + "extension", "foo", + "description", "bar" + )); + + JPackageCommand cmd = JPackageCommand.helloAppImage() + .addArguments("--resource-dir", + getResourceDirWithCustomInfoPList(APP_PLIST_KEY, + includeMainPList, includeRuntimePList)) + .addArguments("--file-associations", propFile); + + cmd.executeAndAssertHelloAppImageCreated(); + + var appPList = MacHelper.readPListFromAppImage(cmd.outputBundle()); + if (includeMainPList) { + validateInfoPListFileKey(appPList, Optional.of(APP_PLIST_KEY)); + validateInfoPList(appPList, appKeyValue); + validateInfoPListFA(appPList, faKeysAndValues); + } else { + validateInfoPListFileKey(appPList, Optional.empty()); + } + + var runtimePList = MacHelper.readPListFromEmbeddedRuntime(cmd.outputBundle()); + if (includeRuntimePList) { + validateInfoPListFileKey(runtimePList, Optional.of(EMBEDDED_RUNTIME_PLIST_KEY)); + validateInfoPList(runtimePList, embeddedRuntimeKeyValue); + } else { + validateInfoPListFileKey(runtimePList, Optional.empty()); + } + } + + @Test + public void testRuntime() throws IOException { + final var runtimeImage = JPackageCommand.createInputRuntimeImage(); + + final var runtimeBundleWorkDir = TKit.createTempDirectory("runtime-bundle"); + + var cmd = new JPackageCommand() + .useToolProvider(true) + .ignoreDefaultRuntime(true) + .dumpOutput(true) + .setPackageType(PackageType.MAC_DMG) + .setArgumentValue("--name", "foo") + .addArguments("--runtime-image", runtimeImage) + .addArguments("--resource-dir", + getResourceDirWithCustomInfoPList(RUNTIME_PLIST_KEY, true, false)) + .addArguments("--dest", runtimeBundleWorkDir); + + cmd.execute(); + + MacHelper.withExplodedDmg(cmd, dmgImage -> { + if (dmgImage.endsWith(cmd.appInstallationDirectory().getFileName())) { + var runtimePList = MacHelper.readPListFromAppImage(dmgImage); + validateInfoPListFileKey(runtimePList, Optional.of(RUNTIME_PLIST_KEY)); + validateInfoPList(runtimePList, runtimeKeyValue); + } + }); + } +} diff --git a/test/jdk/tools/jpackage/macosx/SigningRuntimeImagePackageTest.java b/test/jdk/tools/jpackage/macosx/SigningRuntimeImagePackageTest.java index 8032a4532e92a..ec9fc67f7252d 100644 --- a/test/jdk/tools/jpackage/macosx/SigningRuntimeImagePackageTest.java +++ b/test/jdk/tools/jpackage/macosx/SigningRuntimeImagePackageTest.java @@ -25,6 +25,7 @@ import java.nio.file.Path; import java.util.function.Predicate; import java.util.stream.Stream; + import jdk.jpackage.test.Annotations.Parameter; import jdk.jpackage.test.Annotations.Test; import jdk.jpackage.test.Executor; @@ -94,32 +95,9 @@ private static JPackageCommand addSignOptions(JPackageCommand cmd, int certIndex return cmd; } - private static Path createInputRuntimeImage() throws IOException { - - final Path runtimeImageDir; - - if (JPackageCommand.DEFAULT_RUNTIME_IMAGE != null) { - runtimeImageDir = JPackageCommand.DEFAULT_RUNTIME_IMAGE; - } else { - runtimeImageDir = TKit.createTempDirectory("runtime-image").resolve("data"); - - new Executor().setToolProvider(JavaTool.JLINK) - .dumpOutput() - .addArguments( - "--output", runtimeImageDir.toString(), - "--add-modules", "java.desktop", - "--strip-debug", - "--no-header-files", - "--no-man-pages") - .execute(); - } - - return runtimeImageDir; - } - private static Path createInputRuntimeBundle(int certIndex) throws IOException { - final var runtimeImage = createInputRuntimeImage(); + final var runtimeImage = JPackageCommand.createInputRuntimeImage(); final var runtimeBundleWorkDir = TKit.createTempDirectory("runtime-bundle"); @@ -178,7 +156,7 @@ public static void test(boolean useJDKBundle, if (useJDKBundle) { inputRuntime[0] = createInputRuntimeBundle(jdkBundleCert.value()); } else { - inputRuntime[0] = createInputRuntimeImage(); + inputRuntime[0] = JPackageCommand.createInputRuntimeImage(); } }) .addInitializer(cmd -> { diff --git a/test/jdk/tools/jpackage/share/RuntimePackageTest.java b/test/jdk/tools/jpackage/share/RuntimePackageTest.java index f66f774b227ac..c6a266261689c 100644 --- a/test/jdk/tools/jpackage/share/RuntimePackageTest.java +++ b/test/jdk/tools/jpackage/share/RuntimePackageTest.java @@ -114,7 +114,7 @@ public static void testName() { } private static PackageTest init() { - return init(RuntimePackageTest::createInputRuntimeImage); + return init(JPackageCommand::createInputRuntimeImage); } private static PackageTest init(ThrowingSupplier createRuntime) { @@ -173,32 +173,9 @@ private static Path inputRuntimeDir(JPackageCommand cmd) { return path; } - private static Path createInputRuntimeImage() throws IOException { - - final Path runtimeImageDir; - - if (JPackageCommand.DEFAULT_RUNTIME_IMAGE != null) { - runtimeImageDir = JPackageCommand.DEFAULT_RUNTIME_IMAGE; - } else { - runtimeImageDir = TKit.createTempDirectory("runtime-image").resolve("data"); - - new Executor().setToolProvider(JavaTool.JLINK) - .dumpOutput() - .addArguments( - "--output", runtimeImageDir.toString(), - "--add-modules", "java.desktop", - "--strip-debug", - "--no-header-files", - "--no-man-pages") - .execute(); - } - - return runtimeImageDir; - } - private static Path createInputRuntimeBundle() throws IOException { - final var runtimeImage = createInputRuntimeImage(); + final var runtimeImage = JPackageCommand.createInputRuntimeImage(); final var runtimeBundleWorkDir = TKit.createTempDirectory("runtime-bundle");