diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/util/PListWriter.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/PListWriter.java similarity index 90% rename from src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/util/PListWriter.java rename to src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/PListWriter.java index 381faee3802e3..a5eb150a1d1ae 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/util/PListWriter.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/PListWriter.java @@ -90,7 +90,13 @@ 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\""); + try { + xml.writeDTD(""); + } catch (UnsupportedOperationException ex) { + // Silently ignore. + // This would normally be thrown by com.sun.xml.internal.stream.writers.XMLDOMWriterImpl.writeDTD() + // or (presumably) any other DOM tree-backed XML stream writer implementation. + } xml.writeStartElement("plist"); xml.writeAttribute("version", "1.0"); content.accept(xml); diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/XmlUtils.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/XmlUtils.java index c78101c124504..549044862d875 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/XmlUtils.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/XmlUtils.java @@ -24,6 +24,8 @@ */ package jdk.jpackage.internal.util; +import static jdk.jpackage.internal.util.function.ExceptionBox.rethrowUnchecked; + import java.io.IOException; import java.io.Writer; import java.lang.reflect.Proxy; @@ -43,6 +45,7 @@ import javax.xml.transform.Source; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMResult; import javax.xml.transform.stax.StAXResult; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; @@ -63,8 +66,7 @@ public static XmlConsumer toXmlConsumer(XmlConsumerNoArg xmlConsumer) { return xml -> xmlConsumer.accept(); } - public static void createXml(Path dstFile, XmlConsumer xmlConsumer) throws - IOException { + public static void createXml(Path dstFile, XmlConsumer xmlConsumer) throws IOException { XMLOutputFactory xmlFactory = XMLOutputFactory.newInstance(); Files.createDirectories(dstFile.getParent()); try (Writer w = Files.newBufferedWriter(dstFile)) { @@ -78,9 +80,28 @@ public static void createXml(Path dstFile, XmlConsumer xmlConsumer) throws xml.flush(); xml.close(); } catch (XMLStreamException ex) { - throw new IOException(ex); - } catch (IOException ex) { - throw ex; + throw rethrowUnchecked(ex); + } + } + + public static void createXml(Node root, XmlConsumer xmlConsumer) throws IOException { + createXml(new DOMResult(root), xmlConsumer); + } + + public static DOMResult createXml(XmlConsumer xmlConsumer) throws IOException { + var dom = new DOMResult(initDocumentBuilder().newDocument()); + createXml(dom, xmlConsumer); + return dom; + } + + public static void createXml(DOMResult dom, XmlConsumer xmlConsumer) throws IOException { + try { + var xml = XMLOutputFactory.newInstance().createXMLStreamWriter(dom); + xmlConsumer.accept(xml); + xml.flush(); + xml.close(); + } catch (XMLStreamException ex) { + throw rethrowUnchecked(ex); } } diff --git a/test/jdk/tools/jpackage/helpers-test/jdk/jpackage/test/MacHelperTest.java b/test/jdk/tools/jpackage/helpers-test/jdk/jpackage/test/MacHelperTest.java new file mode 100644 index 0000000000000..5d14f021eafc9 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers-test/jdk/jpackage/test/MacHelperTest.java @@ -0,0 +1,128 @@ +/* + * 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 java.util.Map.entry; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.io.StringReader; +import java.io.UncheckedIOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import jdk.jpackage.internal.util.PListReader; +import jdk.jpackage.internal.util.XmlUtils; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Node; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +public class MacHelperTest { + + @Test + public void test_flatMapPList() { + var props = MacHelper.flatMapPList(new PListReader(createXml( + "AppName", + "Hello", + "AppVersion", + "1.0", + "UserData", + "", + " Foo", + " ", + " Str", + " ", + " Another Str", + " ", + " ", + " ", + " ", + "", + "Checksum", + "7841ff0076cdde93bdca02cfd332748c40620ce4", + "Plugins", + "", + " ", + " PluginName", + " Foo", + " Priority", + " 13", + " History", + " ", + " New File", + " Another New File", + " ", + " ", + " ", + " PluginName", + " Bar", + " Priority", + " 23", + " History", + " ", + " ", + " ", + "" + ))); + + assertEquals(Map.ofEntries( + entry("/AppName", "Hello"), + entry("/AppVersion", "1.0"), + entry("/UserData/Foo[0]", "Str"), + entry("/UserData/Foo[1][0]", "Another Str"), + entry("/UserData/Foo[1][1]", "true"), + entry("/UserData/Foo[1][2]", "false"), + entry("/Checksum", "7841ff0076cdde93bdca02cfd332748c40620ce4"), + entry("/Plugins[0]/PluginName", "Foo"), + entry("/Plugins[0]/Priority", "13"), + entry("/Plugins[0]/History[0]", "New File"), + entry("/Plugins[0]/History[1]", "Another New File"), + entry("/Plugins[1]/PluginName", "Bar"), + entry("/Plugins[1]/Priority", "23"), + entry("/Plugins[1]/History[]", ""), + entry("/Plugins[2]{}", "") + ), props); + } + + private static String createPListXml(String ...xml) { + final List content = new ArrayList<>(); + content.add(""); + content.add(""); + content.add(""); + content.addAll(List.of(xml)); + content.add(""); + content.add(""); + return String.join("", content.toArray(String[]::new)); + } + + private static Node createXml(String ...xml) { + try { + return XmlUtils.initDocumentBuilder().parse(new InputSource(new StringReader(createPListXml(xml)))); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } catch (SAXException ex) { + throw new RuntimeException(ex); + } + } +} 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..8984450f54be2 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java @@ -313,7 +313,7 @@ public JPackageCommand setFakeRuntime() { // an error by PackageTest. createBulkFile.accept(fakeRuntimeDir.resolve(Path.of("bin", "bulk"))); - cmd.addArguments("--runtime-image", fakeRuntimeDir); + cmd.setArgumentValue("--runtime-image", fakeRuntimeDir); }); return this; @@ -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..1b18b0512ae39 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java @@ -23,22 +23,41 @@ package jdk.jpackage.test; import static java.util.stream.Collectors.toSet; +import static jdk.jpackage.internal.util.PListWriter.writeArray; +import static jdk.jpackage.internal.util.PListWriter.writeBoolean; +import static jdk.jpackage.internal.util.PListWriter.writeBooleanOptional; +import static jdk.jpackage.internal.util.PListWriter.writeDict; +import static jdk.jpackage.internal.util.PListWriter.writeKey; +import static jdk.jpackage.internal.util.PListWriter.writeString; +import static jdk.jpackage.internal.util.PListWriter.writeStringArray; +import static jdk.jpackage.internal.util.PListWriter.writeStringOptional; +import static jdk.jpackage.internal.util.XmlUtils.toXmlConsumer; +import static jdk.jpackage.internal.util.function.ThrowingRunnable.toRunnable; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.constant.ClassDesc; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; +import java.util.Properties; import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.xml.stream.XMLStreamWriter; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathFactory; import jdk.jpackage.internal.RetryExecutor; @@ -160,6 +179,52 @@ public static PListReader readPList(Stream lines) { .collect(Collectors.joining()).getBytes(StandardCharsets.UTF_8))).get(); } + public static Map flatMapPList(PListReader plistReader) { + return Collections.unmodifiableMap(expandPListDist(new HashMap<>(), "", plistReader.toMap(true))); + } + + private static Map expandPListDist(Map accumulator, String root, Map plistDict) { + Objects.requireNonNull(accumulator); + Objects.requireNonNull(plistDict); + Objects.requireNonNull(root); + for (var e : plistDict.entrySet()) { + collectPListProperty(accumulator, root + "/" + e.getKey(), e.getValue()); + } + return accumulator; + } + + @SuppressWarnings("unchecked") + private static void collectPListProperty(Map accumulator, String key, Object value) { + Objects.requireNonNull(accumulator); + Objects.requireNonNull(key); + Objects.requireNonNull(value); + switch (value) { + case PListReader.Raw raw -> { + accumulator.put(key, raw.value()); + } + case List array -> { + if (array.isEmpty()) { + accumulator.put(key + "[]", ""); + } else { + for (int i = 0; i != array.size(); i++) { + collectPListProperty(accumulator, String.format("%s[%d]", key, i), array.get(i)); + } + } + } + case Map map -> { + if (map.isEmpty()) { + accumulator.put(key + "{}", ""); + } else { + expandPListDist(accumulator, key, (Map)map); + } + } + default -> { + throw new IllegalArgumentException(String.format( + "Unexpected value type [%s] of property [%s]", value.getClass(), key)); + } + } + } + public static boolean signPredefinedAppImage(JPackageCommand cmd) { Objects.requireNonNull(cmd); if (!TKit.isOSX()) { @@ -186,6 +251,83 @@ public static boolean appImageSigned(JPackageCommand cmd) { return (cmd.hasArgument("--mac-signing-key-user-name") || cmd.hasArgument("--mac-app-image-sign-identity")); } + public static void writeFaPListFragment(JPackageCommand cmd, XMLStreamWriter xml) { + toRunnable(() -> { + var allProps = Stream.of(cmd.getAllArgumentValues("--file-associations")).map(Path::of).map(propFile -> { + try (var propFileReader = Files.newBufferedReader(propFile)) { + var props = new Properties(); + props.load(propFileReader); + return props; + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }).toList(); + + if (!allProps.isEmpty()) { + var bundleId = getPackageId(cmd); + + Function contentType = fa -> { + return String.format("%s.%s", bundleId, Objects.requireNonNull(fa.getProperty("extension"))); + }; + + Function> icon = fa -> { + return Optional.ofNullable(fa.getProperty("icon")).map(Path::of).map(Path::getFileName).map(Path::toString); + }; + + BiFunction> asBoolean = (fa, key) -> { + return Optional.ofNullable(fa.getProperty(key)).map(Boolean::parseBoolean); + }; + + BiFunction> asList = (fa, key) -> { + return Optional.ofNullable(fa.getProperty(key)).map(str -> { + return List.of(str.split("[ ,]+")); + }).orElseGet(List::of); + }; + + writeKey(xml, "CFBundleDocumentTypes"); + writeArray(xml, toXmlConsumer(() -> { + for (var fa : allProps) { + writeDict(xml, toXmlConsumer(() -> { + writeStringArray(xml, "LSItemContentTypes", List.of(contentType.apply(fa))); + writeStringOptional(xml, "CFBundleTypeName", Optional.ofNullable(fa.getProperty("description"))); + writeString(xml, "LSHandlerRank", Optional.ofNullable(fa.getProperty("mac.LSHandlerRank")).orElse("Owner")); + writeString(xml, "CFBundleTypeRole", Optional.ofNullable(fa.getProperty("mac.CFBundleTypeRole")).orElse("Editor")); + writeStringOptional(xml, "NSPersistentStoreTypeKey", Optional.ofNullable(fa.getProperty("mac.NSPersistentStoreTypeKey"))); + writeStringOptional(xml, "NSDocumentClass", Optional.ofNullable(fa.getProperty("mac.NSDocumentClass"))); + writeBoolean(xml, "LSIsAppleDefaultForType", true); + writeBooleanOptional(xml, "LSTypeIsPackage", asBoolean.apply(fa, "mac.LSTypeIsPackage")); + writeBooleanOptional(xml, "LSSupportsOpeningDocumentsInPlace", asBoolean.apply(fa, "mac.LSSupportsOpeningDocumentsInPlace")); + writeBooleanOptional(xml, "UISupportsDocumentBrowser", asBoolean.apply(fa, "mac.UISupportsDocumentBrowser")); + writeStringOptional(xml, "CFBundleTypeIconFile", icon.apply(fa)); + })); + } + })); + + writeKey(xml, "UTExportedTypeDeclarations"); + writeArray(xml, toXmlConsumer(() -> { + for (var fa : allProps) { + writeDict(xml, toXmlConsumer(() -> { + writeString(xml, "UTTypeIdentifier", contentType.apply(fa)); + writeStringOptional(xml, "UTTypeDescription", Optional.ofNullable(fa.getProperty("description"))); + if (fa.containsKey("mac.UTTypeConformsTo")) { + writeStringArray(xml, "UTTypeConformsTo", asList.apply(fa, "mac.UTTypeConformsTo")); + } else { + writeStringArray(xml, "UTTypeConformsTo", List.of("public.data")); + } + writeStringOptional(xml, "UTTypeIconFile", icon.apply(fa)); + writeKey(xml, "UTTypeTagSpecification"); + writeDict(xml, toXmlConsumer(() -> { + writeStringArray(xml, "public.filename-extension", List.of(fa.getProperty("extension"))); + writeStringArray(xml, "public.mime-type", List.of(fa.getProperty("mime-type"))); + writeStringArray(xml, "NSExportableTypes", asList.apply(fa, "mac.NSExportableTypes")); + })); + })); + } + })); + } + }).run(); + } + static PackageHandlers createDmgPackageHandlers() { return new PackageHandlers(MacHelper::installDmg, MacHelper::uninstallDmg, MacHelper::unpackDmg); } @@ -375,7 +517,12 @@ private static String getPackageName(JPackageCommand cmd) { private static String getPackageId(JPackageCommand cmd) { return cmd.getArgumentValue("--mac-package-identifier", () -> { return cmd.getArgumentValue("--main-class", cmd::name, className -> { - return JavaAppDesc.parse(className).packageName(); + var packageName = ClassDesc.of(className).packageName(); + if (packageName.isEmpty()) { + return className; + } else { + return packageName; + } }); }); } diff --git a/test/jdk/tools/jpackage/macosx/CustomInfoPListTest.java b/test/jdk/tools/jpackage/macosx/CustomInfoPListTest.java new file mode 100644 index 0000000000000..f8e606d77e8a0 --- /dev/null +++ b/test/jdk/tools/jpackage/macosx/CustomInfoPListTest.java @@ -0,0 +1,373 @@ +/* + * 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. + */ + +import static java.util.Map.entry; +import static jdk.jpackage.internal.util.PListWriter.writeDict; +import static jdk.jpackage.internal.util.PListWriter.writePList; +import static jdk.jpackage.internal.util.PListWriter.writeString; +import static jdk.jpackage.internal.util.XmlUtils.createXml; +import static jdk.jpackage.internal.util.XmlUtils.toXmlConsumer; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import jdk.jpackage.internal.util.PListReader; +import jdk.jpackage.internal.util.function.ThrowingBiConsumer; +import jdk.jpackage.internal.util.function.ThrowingConsumer; +import jdk.jpackage.test.Annotations.ParameterSupplier; +import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.JPackageStringBundle; +import jdk.jpackage.test.MacHelper; +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.PackageType; +import jdk.jpackage.test.RunnablePackageTest.Action; +import jdk.jpackage.test.TKit; + +/** + * 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 + */ +public class CustomInfoPListTest { + + @Test + @ParameterSupplier("customPLists") + public void testAppImage(TestConfig cfg) throws Throwable { + var cmd = cfg.init(JPackageCommand.helloAppImage()); + var verifier = cfg.createPListFilesVerifier(cmd.executePrerequisiteActions()); + cmd.executeAndAssertHelloAppImageCreated(); + verifier.accept(cmd); + } + + @Test + @ParameterSupplier("customPLists") + public void testNativePackage(TestConfig cfg) { + List> verifier = new ArrayList<>(); + new PackageTest().configureHelloApp().addInitializer(cmd -> { + cfg.init(cmd.setFakeRuntime()); + }).addRunOnceInitializer(() -> { + verifier.add(cfg.createPListFilesVerifier(JPackageCommand.helloAppImage().executePrerequisiteActions())); + }).addInstallVerifier(cmd -> { + verifier.get(0).accept(cmd); + }).run(Action.CREATE_AND_UNPACK); + } + + @Test + public void testRuntime() { + final Path runtimeImage[] = new Path[1]; + + var cfg = new TestConfig(Set.of(CustomPListType.RUNTIME)); + + new PackageTest().addRunOnceInitializer(() -> { + runtimeImage[0] = JPackageCommand.createInputRuntimeImage(); + }).addInitializer(cmd -> { + cmd.ignoreDefaultRuntime(true) + .removeArgumentWithValue("--input") + .setArgumentValue("--runtime-image", runtimeImage[0]); + cfg.init(cmd); + }).addInstallVerifier(cmd -> { + cfg.createPListFilesVerifier(cmd).accept(cmd); + }).run(Action.CREATE_AND_UNPACK); + } + + public static Collection customPLists() { + return Stream.of( + Set.of(CustomPListType.APP), + Set.of(CustomPListType.APP_WITH_FA), + Set.of(CustomPListType.EMBEDDED_RUNTIME), + Set.of(CustomPListType.APP, CustomPListType.EMBEDDED_RUNTIME) + ).map(TestConfig::new).map(cfg -> { + return new Object[] { cfg }; + }).toList(); + } + + private static List toStringList(PListReader plistReader) { + return MacHelper.flatMapPList(plistReader).entrySet().stream().sorted(Comparator.comparing(Map.Entry::getKey)).map(e -> { + return String.format("%s: %s", e.getKey(), e.getValue()); + }).toList(); + } + + + public record TestConfig(Set customPLists) { + + public TestConfig { + Objects.requireNonNull(customPLists); + if (customPLists.isEmpty()) { + throw new IllegalArgumentException(); + } + } + + @Override + public String toString() { + return customPLists.stream() + .sorted(Comparator.comparing(CustomPListType::role)) + .map(CustomPListType::toString) + .collect(Collectors.joining("+")); + } + + JPackageCommand init(JPackageCommand cmd) throws IOException { + if (customPLists.contains(CustomPListType.APP_WITH_FA)) { + final Path propFile = TKit.createTempFile("fa.properties"); + var map = Map.ofEntries( + entry("mime-type", "application/x-jpackage-foo"), + entry("extension", "foo"), + entry("description", "bar") + ); + TKit.createPropertiesFile(propFile, map); + cmd.setArgumentValue("--file-associations", propFile); + } + + cmd.setArgumentValue("--resource-dir", TKit.createTempDirectory("resources")); + for (var customPList : customPLists) { + customPList.createInputPListFile(cmd); + } + return cmd; + } + + ThrowingConsumer createPListFilesVerifier(JPackageCommand cmd) throws IOException { + ThrowingConsumer defaultVerifier = otherCmd -> { + for (var customPList : customPLists) { + customPList.verifyPListFile(otherCmd); + } + }; + + var defaultPListFiles = CustomPListType.defaultRoles(customPLists); + + if (defaultPListFiles.isEmpty()) { + return defaultVerifier; + } else { + var vanillaCmd = new JPackageCommand().setFakeRuntime() + .addArguments(cmd.getAllArguments()) + .setPackageType(PackageType.IMAGE) + .removeArgumentWithValue("--resource-dir") + .setArgumentValue("--dest", TKit.createTempDirectory("vanilla")); + vanillaCmd.executeIgnoreExitCode().assertExitCodeIsZero(); + + return otherCmd -> { + defaultVerifier.accept(otherCmd); + for (var defaultPListFile : defaultPListFiles) { + final var expectedPListPath = defaultPListFile.path(vanillaCmd); + final var expectedPList = MacHelper.readPList(expectedPListPath); + + final var actualPListPath = defaultPListFile.path(otherCmd); + final var actualPList = MacHelper.readPList(actualPListPath); + + var expected = toStringList(expectedPList); + var actual = toStringList(actualPList); + + TKit.assertStringListEquals(expected, actual, String.format( + "Check contents of [%s] and [%s] plist files are the same", expectedPListPath, actualPListPath)); + } + }; + } + } + } + + + private enum PListRole { + MAIN, + EMBEDDED_RUNTIME, + ; + + Path path(JPackageCommand cmd) { + final Path bundleRoot; + if (cmd.isRuntime() || this == EMBEDDED_RUNTIME) { + bundleRoot = cmd.appRuntimeDirectory(); + } else { + bundleRoot = cmd.appLayout().contentDirectory().getParent(); + } + return bundleRoot.resolve("Contents/Info.plist"); + } + } + + + private enum CustomPListType { + APP( + CustomPListFactory.PLIST_INPUT::writeAppPlist, + CustomPListFactory.PLIST_OUTPUT::writeAppPlist, + "Info.plist"), + + APP_WITH_FA(APP), + + EMBEDDED_RUNTIME( + CustomPListFactory.PLIST_INPUT::writeEmbeddedRuntimePlist, + CustomPListFactory.PLIST_OUTPUT::writeEmbeddedRuntimePlist, + "Runtime-Info.plist"), + + RUNTIME( + CustomPListFactory.PLIST_INPUT::writeRuntimePlist, + CustomPListFactory.PLIST_OUTPUT::writeRuntimePlist, + "Info.plist"), + ; + + private CustomPListType( + ThrowingBiConsumer inputPlistWriter, + ThrowingBiConsumer outputPlistWriter, + String outputPlistFilename) { + this.inputPlistWriter = ThrowingBiConsumer.toBiConsumer(inputPlistWriter); + this.outputPlistWriter = ThrowingBiConsumer.toBiConsumer(outputPlistWriter); + this.outputPlistFilename = outputPlistFilename; + } + + private CustomPListType(CustomPListType other) { + this.inputPlistWriter = other.inputPlistWriter; + this.outputPlistWriter = other.outputPlistWriter; + this.outputPlistFilename = other.outputPlistFilename; + } + + void createInputPListFile(JPackageCommand cmd) throws IOException { + createXml(Path.of(cmd.getArgumentValue("--resource-dir")).resolve(outputPlistFilename), xml -> { + inputPlistWriter.accept(cmd, xml); + }); + } + + void verifyPListFile(JPackageCommand cmd) throws IOException { + final var expectedPList = new PListReader(createXml(xml -> { + outputPlistWriter.accept(cmd, xml); + }).getNode()); + + final var actualPListPath = role().path(cmd); + final var actualPList = MacHelper.readPList(actualPListPath); + + var expected = toStringList(expectedPList); + var actual = toStringList(actualPList); + + TKit.assertStringListEquals(expected, actual, String.format("Check contents of [%s] plist file is as expected", actualPListPath)); + } + + PListRole role() { + if (this == EMBEDDED_RUNTIME) { + return PListRole.EMBEDDED_RUNTIME; + } else { + return PListRole.MAIN; + } + } + + static Set defaultRoles(Collection customPLists) { + var result = new HashSet<>(Set.of(PListRole.values())); + customPLists.stream().mapMulti((customPList, acc) -> { + if (customPList == CustomPListType.RUNTIME) { + List.of(PListRole.values()).forEach(acc::accept); + } else { + acc.accept(customPList.role()); + } + }).forEach(result::remove); + return Collections.unmodifiableSet(result); + } + + private final BiConsumer inputPlistWriter; + private final BiConsumer outputPlistWriter; + private final String outputPlistFilename; + } + + + private enum CustomPListFactory { + PLIST_INPUT, + PLIST_OUTPUT, + ; + + private void writeAppPlist(JPackageCommand cmd, XMLStreamWriter xml) throws XMLStreamException, IOException { + writePList(xml, toXmlConsumer(() -> { + writeDict(xml, toXmlConsumer(() -> { + writeString(xml, "CustomAppProperty", "App"); + writeString(xml, "CFBundleExecutable", value("DEPLOY_LAUNCHER_NAME", cmd.name())); + writeString(xml, "CFBundleIconFile", value("DEPLOY_ICON_FILE", cmd.name() + ".icns")); + writeString(xml, "CFBundleIdentifier", value("DEPLOY_BUNDLE_IDENTIFIER", "Hello")); + writeString(xml, "CFBundleName", value("DEPLOY_BUNDLE_NAME", cmd.name())); + writeString(xml, "CFBundleShortVersionString", value("DEPLOY_BUNDLE_SHORT_VERSION", cmd.version())); + writeString(xml, "LSApplicationCategoryType", value("DEPLOY_APP_CATEGORY", "public.app-category.utilities")); + writeString(xml, "CFBundleVersion", value("DEPLOY_BUNDLE_CFBUNDLE_VERSION", cmd.version())); + writeString(xml, "NSHumanReadableCopyright", value("DEPLOY_BUNDLE_COPYRIGHT", + JPackageStringBundle.MAIN.cannedFormattedString("param.copyright.default", new Date()).getValue())); + if (cmd.hasArgument("--file-associations")) { + if (this == PLIST_INPUT) { + xml.writeCharacters("DEPLOY_FILE_ASSOCIATIONS"); + } else { + MacHelper.writeFaPListFragment(cmd, xml); + } + } + })); + })); + } + + void writeEmbeddedRuntimePlist(JPackageCommand cmd, XMLStreamWriter xml) throws XMLStreamException, IOException { + writePList(xml, toXmlConsumer(() -> { + writeDict(xml, toXmlConsumer(() -> { + writeString(xml, "CustomEmbeddedRuntimeProperty", "Embedded runtime"); + writeString(xml, "CFBundleIdentifier", value("CF_BUNDLE_IDENTIFIER", "Hello")); + writeString(xml, "CFBundleName", value("CF_BUNDLE_NAME", cmd.name())); + writeString(xml, "CFBundleShortVersionString", value("CF_BUNDLE_SHORT_VERSION_STRING", cmd.version())); + writeString(xml, "CFBundleVersion", value("CF_BUNDLE_VERSION", cmd.version())); + })); + })); + } + + void writeRuntimePlist(JPackageCommand cmd, XMLStreamWriter xml) throws XMLStreamException, IOException { + writePList(xml, toXmlConsumer(() -> { + writeDict(xml, toXmlConsumer(() -> { + writeString(xml, "CustomRuntimeProperty", "Runtime"); + writeString(xml, "CFBundleIdentifier", value("CF_BUNDLE_IDENTIFIER", cmd.name())); + writeString(xml, "CFBundleName", value("CF_BUNDLE_NAME", cmd.name())); + writeString(xml, "CFBundleShortVersionString", value("CF_BUNDLE_SHORT_VERSION_STRING", cmd.version())); + writeString(xml, "CFBundleVersion", value("CF_BUNDLE_VERSION", cmd.version())); + writeString(xml, "CustomInfoPListFA", "DEPLOY_FILE_ASSOCIATIONS"); + })); + })); + } + + private String value(String input, String output) { + if (this == PLIST_INPUT) { + return input; + } else { + return output; + } + } + } +} diff --git a/test/jdk/tools/jpackage/macosx/MacFileAssociationsTest.java b/test/jdk/tools/jpackage/macosx/MacFileAssociationsTest.java index e6d8b41f05fbb..3277b56ab46cc 100644 --- a/test/jdk/tools/jpackage/macosx/MacFileAssociationsTest.java +++ b/test/jdk/tools/jpackage/macosx/MacFileAssociationsTest.java @@ -22,14 +22,22 @@ */ import static java.util.Map.entry; +import static jdk.jpackage.internal.util.PListWriter.writeDict; +import static jdk.jpackage.internal.util.PListWriter.writePList; +import static jdk.jpackage.internal.util.XmlUtils.createXml; +import static jdk.jpackage.internal.util.XmlUtils.toXmlConsumer; +import static jdk.jpackage.test.MacHelper.flatMapPList; +import static jdk.jpackage.test.MacHelper.readPListFromAppImage; +import static jdk.jpackage.test.MacHelper.writeFaPListFragment; import java.nio.file.Path; -import java.util.List; +import java.util.Comparator; import java.util.Map; +import java.util.function.Function; +import java.util.stream.Stream; import jdk.jpackage.internal.util.PListReader; import jdk.jpackage.test.Annotations.Test; import jdk.jpackage.test.JPackageCommand; -import jdk.jpackage.test.MacHelper; import jdk.jpackage.test.TKit; /** @@ -52,7 +60,7 @@ public class MacFileAssociationsTest { @Test public static void test() throws Exception { - final Path propFile = TKit.workDir().resolve("fa.properties"); + final Path propFile = TKit.createTempFile("fa.properties"); Map map = Map.ofEntries( entry("mime-type", "application/x-jpackage-foo"), entry("extension", "foo"), @@ -67,55 +75,26 @@ public static void test() throws Exception { entry("mac.UTTypeConformsTo", "public.image, public.data")); TKit.createPropertiesFile(propFile, map); - JPackageCommand cmd = JPackageCommand.helloAppImage(); + final var cmd = JPackageCommand.helloAppImage().setFakeRuntime(); cmd.addArguments("--file-associations", propFile); cmd.executeAndAssertHelloAppImageCreated(); - Path appImage = cmd.outputBundle(); - verifyPList(appImage); - } - - private static void checkStringValue(PListReader plist, String key, String value) { - String result = plist.queryValue(key); - TKit.assertEquals(value, result, String.format( - "Check value of %s plist key", key)); - } - - private static void checkBoolValue(PListReader plist, String key, boolean value) { - boolean result = plist.queryBoolValue(key); - TKit.assertEquals(value, result, String.format( - "Check value of %s plist key", key)); - } - - private static void checkArrayValue(PListReader plist, String key, - List values) { - List result = plist.queryStringArrayValue(key); - TKit.assertStringListEquals(values, result, String.format( - "Check value of %s plist key", key)); - } - - private static void verifyPList(Path appImage) throws Exception { - final var rootPlist = MacHelper.readPListFromAppImage(appImage); - - TKit.traceFileContents(appImage.resolve("Contents/Info.plist"), "Info.plist"); - - var plist = rootPlist.queryArrayValue("CFBundleDocumentTypes", false).findFirst().map(PListReader.class::cast).orElseThrow(); - - checkStringValue(plist, "CFBundleTypeRole", "Viewer"); - checkStringValue(plist, "LSHandlerRank", "Default"); - checkStringValue(plist, "NSDocumentClass", "SomeClass"); - - checkBoolValue(plist, "LSTypeIsPackage", true); - checkBoolValue(plist, "LSSupportsOpeningDocumentsInPlace", false); - checkBoolValue(plist, "UISupportsDocumentBrowser", false); - - plist = rootPlist.queryArrayValue("UTExportedTypeDeclarations", false).findFirst().map(PListReader.class::cast).orElseThrow(); - - checkArrayValue(plist, "UTTypeConformsTo", List.of("public.image", "public.data")); + Function, String> toString = e -> { + return String.format("%s => %s", e.getKey(), e.getValue()); + }; - plist = plist.queryDictValue("UTTypeTagSpecification"); + final var actualFaProperties = flatMapPList(readPListFromAppImage(cmd.outputBundle())).entrySet().stream().filter(e -> { + return Stream.of("/CFBundleDocumentTypes", "/UTExportedTypeDeclarations").anyMatch(e.getKey()::startsWith); + }).sorted(Comparator.comparing(Map.Entry::getKey)).map(toString).toList(); - checkArrayValue(plist, "NSExportableTypes", List.of("public.png", "public.jpg")); + final var expectedFaProperties = flatMapPList(new PListReader(createXml(xml -> { + writePList(xml, toXmlConsumer(() -> { + writeDict(xml, toXmlConsumer(() -> { + writeFaPListFragment(cmd, xml); + })); + })); + }).getNode())).entrySet().stream().sorted(Comparator.comparing(Map.Entry::getKey)).map(toString).toList(); + TKit.assertStringListEquals(expectedFaProperties, actualFaProperties, "Check fa properties in the Info.plist file as expected"); } } diff --git a/test/jdk/tools/jpackage/macosx/SigningRuntimeImagePackageTest.java b/test/jdk/tools/jpackage/macosx/SigningRuntimeImagePackageTest.java index 8032a4532e92a..a045d27917704 100644 --- a/test/jdk/tools/jpackage/macosx/SigningRuntimeImagePackageTest.java +++ b/test/jdk/tools/jpackage/macosx/SigningRuntimeImagePackageTest.java @@ -29,7 +29,6 @@ import jdk.jpackage.test.Annotations.Test; import jdk.jpackage.test.Executor; import jdk.jpackage.test.JPackageCommand; -import jdk.jpackage.test.JavaTool; import jdk.jpackage.test.MacHelper; import jdk.jpackage.test.PackageTest; import jdk.jpackage.test.PackageType; @@ -94,32 +93,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 +154,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");