diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageSigner.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageSigner.java index 1345597e352..71f87dd8705 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageSigner.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageSigner.java @@ -127,7 +127,12 @@ final class AppImageSigner { // Sign runtime root directory if present app.asApplicationLayout().map(appLayout -> { return appLayout.resolveAt(appImage.root()); - }).map(MacApplicationLayout.class::cast).map(MacApplicationLayout::runtimeRootDirectory).ifPresent(codesigners); + }).map(MacApplicationLayout.class::cast) + .map(MacApplicationLayout::runtimeRootDirectory) + .flatMap(MacBundle::fromPath) + .filter(MacBundle::isValid) + .map(MacBundle::root) + .ifPresent(codesigners); final var frameworkPath = appImage.contentsDir().resolve("Frameworks"); if (Files.isDirectory(frameworkPath)) { diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPackagingPipeline.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPackagingPipeline.java index a6ed164d9f7..6e63b73674e 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPackagingPipeline.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPackagingPipeline.java @@ -133,7 +133,7 @@ final class MacPackagingPipeline { .addDependencies(CopyAppImageTaskID.COPY) .addDependents(PrimaryTaskID.COPY_APP_IMAGE).add() .task(MacCopyAppImageTaskID.COPY_RUNTIME_INFO_PLIST) - .appImageAction(MacPackagingPipeline::writeRuntimeInfoPlist) + .noaction() .addDependencies(CopyAppImageTaskID.COPY) .addDependents(PrimaryTaskID.COPY_APP_IMAGE).add() .task(MacCopyAppImageTaskID.COPY_RUNTIME_JLILIB) @@ -186,14 +186,18 @@ final class MacPackagingPipeline { disabledTasks.add(MacCopyAppImageTaskID.COPY_PACKAGE_FILE); if (predefinedRuntimeBundle.isPresent()) { - // The predefined app image is a macOS bundle. + // The input runtime image is a macOS bundle. // Disable all alterations of the input bundle, but keep the signing enabled. disabledTasks.addAll(List.of(MacCopyAppImageTaskID.values())); disabledTasks.remove(MacCopyAppImageTaskID.COPY_SIGN); + } else { + // The input runtime is not a macOS bundle and doesn't have the plist file. Create one. + builder.task(MacCopyAppImageTaskID.COPY_RUNTIME_INFO_PLIST) + .appImageAction(MacPackagingPipeline::writeRuntimeInfoPlist).add(); } if (predefinedRuntimeBundle.map(MacBundle::isSigned).orElse(false) && !((MacPackage)p).app().sign()) { - // The predefined app image is a signed bundle; explicit signing is not requested for the package. + // The input runtime is a signed bundle; explicit signing is not requested for the package. // Disable the signing, i.e. don't re-sign the input bundle. disabledTasks.add(MacCopyAppImageTaskID.COPY_SIGN); } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PackagingPipeline.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PackagingPipeline.java index 64a37878bd8..6f4e0d0d2d8 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PackagingPipeline.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/PackagingPipeline.java @@ -431,6 +431,10 @@ final class PackagingPipeline { this.taskGraph = Objects.requireNonNull(taskGraph); this.taskConfig = Objects.requireNonNull(taskConfig); this.contextMapper = Objects.requireNonNull(contextMapper); + + if (TRACE_TASK_GRAPTH) { + taskGraph.dumpToStdout(); + } } private TaskContext createTaskContext(BuildEnv env, Application app) { @@ -630,9 +634,27 @@ final class PackagingPipeline { Objects.requireNonNull(id); Objects.requireNonNull(config); return () -> { - if (config.action.isPresent() && context.test(id)) { + + final var withAction = config.action.isPresent(); + final var accepted = withAction && context.test(id); + + if (TRACE_TASK_ACTION) { + var sb = new StringBuffer(); + sb.append("Execute task=[").append(id).append("]: "); + if (!withAction) { + sb.append("no action"); + } else if (!accepted) { + sb.append("rejected"); + } else { + sb.append("run"); + } + System.out.println(sb); + } + + if (accepted) { context.execute(config.action.orElseThrow()); } + return null; }; } @@ -640,4 +662,7 @@ final class PackagingPipeline { private final FixedDAG taskGraph; private final Map taskConfig; private final UnaryOperator contextMapper; + + private static final boolean TRACE_TASK_GRAPTH = false; + private static final boolean TRACE_TASK_ACTION = false; } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/PListReader.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/PListReader.java index 6d7532bbd74..2c693939dab 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/PListReader.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/PListReader.java @@ -36,6 +36,7 @@ import java.util.Objects; import java.util.Optional; import java.util.stream.Stream; import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.dom.DOMSource; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathFactory; @@ -160,15 +161,13 @@ public final class PListReader { * name in the underlying "dict" element */ public String queryValue(String keyName) { - final var node = getNode(keyName); - switch (node.getNodeName()) { - case "string" -> { - return node.getTextContent(); - } - default -> { - throw new NoSuchElementException(); - } - } + return findValue(keyName).orElseThrow(NoSuchElementException::new); + } + + public Optional findValue(String keyName) { + return findNode(keyName).filter(node -> { + return "string".equals(node.getNodeName()); + }).map(Node::getTextContent); } /** @@ -182,15 +181,13 @@ public final class PListReader { * name in the underlying "dict" element */ public PListReader queryDictValue(String keyName) { - final var node = getNode(keyName); - switch (node.getNodeName()) { - case "dict" -> { - return new PListReader(node); - } - default -> { - throw new NoSuchElementException(); - } - } + return findDictValue(keyName).orElseThrow(NoSuchElementException::new); + } + + public Optional findDictValue(String keyName) { + return findNode(keyName).filter(node -> { + return "dict".equals(node.getNodeName()); + }).map(PListReader::new); } /** @@ -204,18 +201,20 @@ public final class PListReader { * name in the underlying "dict" element */ public boolean queryBoolValue(String keyName) { - final var node = getNode(keyName); - switch (node.getNodeName()) { - case "true" -> { - return true; + return findBoolValue(keyName).orElseThrow(NoSuchElementException::new); + } + + public Optional findBoolValue(String keyName) { + return findNode(keyName).filter(node -> { + switch (node.getNodeName()) { + case "true", "false" -> { + return true; + } + default -> { + return false; + } } - case "false" -> { - return false; - } - default -> { - throw new NoSuchElementException(); - } - } + }).map(Node::getNodeName).map(Boolean::parseBoolean); } /** @@ -233,14 +232,20 @@ public final class PListReader { * name in the underlying "dict" element */ public List queryStringArrayValue(String keyName) { - return queryArrayValue(keyName, false).map(v -> { - if (v instanceof Raw r) { - if (r.type() == Raw.Type.STRING) { - return r.value(); + return findStringArrayValue(keyName).orElseThrow(NoSuchElementException::new); + } + + public Optional> findStringArrayValue(String keyName) { + return findArrayValue(keyName, false).map(stream -> { + return stream.map(v -> { + if (v instanceof Raw r) { + if (r.type() == Raw.Type.STRING) { + return r.value(); + } } - } - return (String)null; - }).filter(Objects::nonNull).toList(); + return (String)null; + }).filter(Objects::nonNull).toList(); + }); } /** @@ -266,15 +271,21 @@ public final class PListReader { * in the underlying "dict" element */ public Stream queryArrayValue(String keyName, boolean fetchDictionaries) { - final var node = getNode(keyName); - switch (node.getNodeName()) { - case "array" -> { - return readArray(node, fetchDictionaries); - } - default -> { - throw new NoSuchElementException(); - } - } + return findArrayValue(keyName, fetchDictionaries).orElseThrow(NoSuchElementException::new); + } + + public Optional> findArrayValue(String keyName, boolean fetchDictionaries) { + return findNode(keyName).filter(node -> { + return "array".equals(node.getNodeName()); + }).map(node -> { + return readArray(node, fetchDictionaries); + }); + } + + public XmlConsumer toXmlConsumer() { + return xml -> { + XmlUtils.concatXml(xml, new DOMSource(root)); + }; } /** @@ -333,12 +344,12 @@ public final class PListReader { }).filter(Optional::isPresent).map(Optional::get); } - private Node getNode(String keyName) { + private Optional findNode(String keyName) { Objects.requireNonNull(keyName); final var query = String.format("*[preceding-sibling::key = \"%s\"][1]", keyName); return Optional.ofNullable(toSupplier(() -> { return (Node) XPathSingleton.INSTANCE.evaluate(query, root, XPathConstants.NODE); - }).get()).orElseThrow(NoSuchElementException::new); + }).get()); } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/Slot.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/Slot.java new file mode 100644 index 00000000000..0c426749ce4 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/Slot.java @@ -0,0 +1,101 @@ +/* + * 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.util; + +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +/** + * A mutable container object for a value. + *

+ * An alternative for cases where {@link AtomicReference} would be an overkill. + * Sample usage: + * {@snippet : + * void foo(MessageNotifier messageNotifier) { + * var lastMessage = Slot.createEmpty(); + * + * messageNotifier.setListener(msg -> { + * lastMessage.set(msg); + * }).run(); + * + * lastMessage.find().ifPresentOrElse(msg -> { + * System.out.println(String.format("The last message: [%s]", msg)); + * }, () -> { + * System.out.println("No messages received"); + * }); + * } + * + * abstract class MessageNotifier { + * MessageNotifier setListener(Consumer messageConsumer) { + * callback = messageConsumer; + * return this; + * } + * + * void run() { + * for (;;) { + * var msg = fetchNextMessage(); + * msg.ifPresent(callback); + * if (msg.isEmpty()) { + * break; + * } + * } + * } + * + * abstract Optional fetchNextMessage(); + * + * private Consumer callback; + * } + * } + * + * An alternative to the {@code Slot} would be either {@code + * AtomicReference} or a single-element {@code String[]} or any other + * suitable container type. {@code AtomicReference} would be an overkill if + * thread-safety is not a concern and the use of other options would be + * confusing. + * + * @param value type + */ +public final class Slot { + + public static Slot createEmpty() { + + return new Slot<>(); + } + + public T get() { + return find().orElseThrow(); + } + + public Optional find() { + return Optional.ofNullable(value); + } + + public void set(T v) { + value = Objects.requireNonNull(v); + } + + private T value; +} 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 549044862d8..c8761259254 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 @@ -31,7 +31,7 @@ import java.io.Writer; import java.lang.reflect.Proxy; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collection; +import java.util.List; import java.util.Optional; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -105,7 +105,11 @@ public final class XmlUtils { } } - public static void mergeXmls(XMLStreamWriter xml, Collection sources) + public static void concatXml(XMLStreamWriter xml, Source... sources) throws XMLStreamException, IOException { + concatXml(xml, List.of(sources)); + } + + public static void concatXml(XMLStreamWriter xml, Iterable sources) throws XMLStreamException, IOException { xml = (XMLStreamWriter) Proxy.newProxyInstance(XMLStreamWriter.class.getClassLoader(), new Class[]{XMLStreamWriter.class}, diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixLauncherAsService.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixLauncherAsService.java index 3d990b990c2..833219eaa10 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixLauncherAsService.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixLauncherAsService.java @@ -111,7 +111,7 @@ class WixLauncherAsService extends LauncherAsService { sources.add(new DOMSource(n)); } - XmlUtils.mergeXmls(xml, sources); + XmlUtils.concatXml(xml, sources); } catch (SAXException ex) { throw new IOException(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 fcd940e725e..088a9fe3bad 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java @@ -376,7 +376,7 @@ public class JPackageCommand extends CommandArguments { return cmd; } - public static Path createInputRuntimeImage() throws IOException { + public static Path createInputRuntimeImage() { final Path runtimeImageDir; @@ -406,7 +406,7 @@ public class JPackageCommand extends CommandArguments { } public JPackageCommand setDefaultAppName() { - return addArguments("--name", TKit.getCurrentDefaultAppName()); + return setArgumentValue("--name", TKit.getCurrentDefaultAppName()); } /** 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 dc753624a32..5c0914ed2f4 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java @@ -31,6 +31,7 @@ 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.initDocumentBuilder; import static jdk.jpackage.internal.util.XmlUtils.toXmlConsumer; import static jdk.jpackage.internal.util.function.ThrowingRunnable.toRunnable; @@ -60,6 +61,7 @@ import java.util.function.UnaryOperator; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathFactory; @@ -71,6 +73,7 @@ import jdk.jpackage.internal.util.function.ThrowingConsumer; import jdk.jpackage.internal.util.function.ThrowingSupplier; import jdk.jpackage.test.MacSign.CertificateRequest; import jdk.jpackage.test.PackageTest.PackageHandlers; +import org.xml.sax.SAXException; public final class MacHelper { @@ -297,79 +300,160 @@ public final class MacHelper { 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); + if (cmd.hasArgument("--app-image")) { + copyFaPListFragmentFromPredefinedAppImage(cmd, xml); + } else { + createFaPListFragmentFromFaProperties(cmd, xml); + } + }).run(); + } + + private static void createFaPListFragmentFromFaProperties(JPackageCommand cmd, XMLStreamWriter xml) + throws XMLStreamException, IOException { + + 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)); + })); } - }).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) { + 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, "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)); + 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")); })); - } - })); + })); + } + })); + } + } - writeKey(xml, "UTExportedTypeDeclarations"); + private static void copyFaPListFragmentFromPredefinedAppImage(JPackageCommand cmd, XMLStreamWriter xml) + throws IOException, SAXException, XMLStreamException { + + var predefinedAppImage = Path.of(Optional.ofNullable(cmd.getArgumentValue("--app-image")).orElseThrow(IllegalArgumentException::new)); + + var plistPath = ApplicationLayout.macAppImage().resolveAt(predefinedAppImage).contentDirectory().resolve("Info.plist"); + + try (var plistStream = Files.newInputStream(plistPath)) { + var plist = new PListReader(initDocumentBuilder().parse(plistStream)); + + var entries = Stream.of("CFBundleDocumentTypes", "UTExportedTypeDeclarations").map(key -> { + return plist.findArrayValue(key, false).map(stream -> { + return stream.map(PListReader.class::cast).toList(); + }).map(plistList -> { + return Map.entry(key, plistList); + }); + }).filter(Optional::isPresent).map(Optional::get).toList(); + + for (var e : entries) { + writeKey(xml, e.getKey()); 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")); - })); - })); + for (var arrayElement : e.getValue()) { + arrayElement.toXmlConsumer().accept(xml); } })); } - }).run(); + } + } + + public static Path createRuntimeBundle(Consumer mutator) { + return createRuntimeBundle(Optional.of(mutator)); + } + + public static Path createRuntimeBundle() { + return createRuntimeBundle(Optional.empty()); + } + + public static Path createRuntimeBundle(Optional> mutator) { + Objects.requireNonNull(mutator); + + final var runtimeImage = JPackageCommand.createInputRuntimeImage(); + + final var runtimeBundleWorkDir = TKit.createTempDirectory("runtime-bundle"); + + final var unpackadeRuntimeBundleDir = runtimeBundleWorkDir.resolve("unpacked"); + + var cmd = new JPackageCommand() + .useToolProvider(true) + .ignoreDefaultRuntime(true) + .dumpOutput(true) + .setPackageType(PackageType.MAC_DMG) + .setArgumentValue("--name", "foo") + .addArguments("--runtime-image", runtimeImage) + .addArguments("--dest", runtimeBundleWorkDir); + + mutator.ifPresent(cmd::mutate); + + cmd.execute(); + + MacHelper.withExplodedDmg(cmd, dmgImage -> { + if (dmgImage.endsWith(cmd.appInstallationDirectory().getFileName())) { + Executor.of("cp", "-R") + .addArgument(dmgImage) + .addArgument(unpackadeRuntimeBundleDir) + .execute(0); + } + }); + + return unpackadeRuntimeBundleDir; } public static Consumer useKeychain(MacSign.ResolvedKeychain keychain) { diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/PListReaderTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/PListReaderTest.java index fe7d0c63870..620650c83b4 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/PListReaderTest.java +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/PListReaderTest.java @@ -23,6 +23,7 @@ package jdk.jpackage.internal.util; +import static jdk.jpackage.internal.util.XmlUtils.initDocumentBuilder; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -30,6 +31,8 @@ import java.io.IOException; import java.io.StringReader; import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -41,6 +44,7 @@ import java.util.stream.Stream; import javax.xml.parsers.ParserConfigurationException; import jdk.jpackage.internal.util.PListReader.Raw; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.EnumSource.Mode; @@ -277,12 +281,49 @@ public class PListReaderTest { assertEquals("A", actualValue); } - @Test - public void test_toMap() { + @ParameterizedTest + @MethodSource("parsedPLists") + public void test_toMap(ParsedPList data) { + testSpec().xml(data.xml()).expect(data.xmlAsMap()).queryType(QueryType.TO_MAP_RECURSIVE).create().test(); + } - var builder = testSpec(); + @ParameterizedTest + @MethodSource("parsedPLists") + public void test_toConsumer(ParsedPList data, @TempDir Path workDir) throws IOException, SAXException { + var node = createXml(data.xml); - builder.xml( + var srcPList = new PListReader(node); + + var sink = workDir.resolve("sink.xml"); + + XmlUtils.createXml(sink, xml -> { + PListWriter.writePList(xml, srcPList.toXmlConsumer()); + }); + + try (var in = Files.newInputStream(sink)) { + var dstPList = new PListReader(initDocumentBuilder().parse(in)); + + var src = srcPList.toMap(true); + var dst = dstPList.toMap(true); + + assertEquals(data.xmlAsMap(), src); + assertEquals(data.xmlAsMap(), dst); + } + } + + private record ParsedPList(Map xmlAsMap, String... xml) { + ParsedPList { + Objects.requireNonNull(xmlAsMap); + } + + ParsedPList(Map xmlAsMap, List xml) { + this(xmlAsMap, xml.toArray(String[]::new)); + } + } + + private static Stream parsedPLists() { + + var xml = List.of( "AppName", "Hello", "", @@ -367,7 +408,7 @@ public class PListReaderTest { ) ); - builder.expect(expected).queryType(QueryType.TO_MAP_RECURSIVE).create().test(); + return Stream.of(new ParsedPList(expected, xml)); } private static List test() { diff --git a/test/jdk/tools/jpackage/macosx/CustomInfoPListTest.java b/test/jdk/tools/jpackage/macosx/CustomInfoPListTest.java index c0b7b6b6604..b690b69269c 100644 --- a/test/jdk/tools/jpackage/macosx/CustomInfoPListTest.java +++ b/test/jdk/tools/jpackage/macosx/CustomInfoPListTest.java @@ -23,11 +23,13 @@ import static java.util.Collections.unmodifiableSortedSet; import static java.util.Map.entry; +import jdk.jpackage.internal.util.Slot; 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 static jdk.jpackage.internal.util.function.ThrowingConsumer.toConsumer; import java.io.IOException; import java.nio.file.Path; @@ -43,15 +45,17 @@ import java.util.Objects; import java.util.Set; import java.util.TreeSet; import java.util.function.BiConsumer; +import java.util.function.Consumer; 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.Parameter; import jdk.jpackage.test.Annotations.ParameterSupplier; import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.ConfigurationTarget; import jdk.jpackage.test.JPackageCommand; import jdk.jpackage.test.JPackageStringBundle; import jdk.jpackage.test.MacHelper; @@ -80,38 +84,67 @@ 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); + testApp(new ConfigurationTarget(JPackageCommand.helloAppImage()), cfg); } @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())); + public void testPackage(TestConfig cfg) { + testApp(new ConfigurationTarget(new PackageTest().configureHelloApp()), cfg); + } + + @Test + @ParameterSupplier("customPLists") + public void testFromAppImage(TestConfig cfg) { + + var verifier = Slot.>createEmpty(); + + var appImageCmd = JPackageCommand.helloAppImage().setFakeRuntime(); + + new PackageTest().addRunOnceInitializer(() -> { + // Create the input app image with custom plist file(s). + // Call JPackageCommand.executePrerequisiteActions() to initialize + // all command line options. + cfg.init(appImageCmd.executePrerequisiteActions()); + appImageCmd.execute(); + verifier.set(cfg.createPListFilesVerifier(appImageCmd)); + }).addInitializer(cmd -> { + cmd.removeArgumentWithValue("--input").setArgumentValue("--app-image", appImageCmd.outputBundle()); }).addInstallVerifier(cmd -> { - verifier.get(0).accept(cmd); + verifier.get().accept(cmd); }).run(Action.CREATE_AND_UNPACK); } @Test - public void testRuntime() { - final Path runtimeImage[] = new Path[1]; + @Parameter("true") + @Parameter("false") + public void testRuntime(boolean runtimeBundle) { + + var runtimeImage = Slot.createEmpty(); var cfg = new TestConfig(Set.of(CustomPListType.RUNTIME)); new PackageTest().addRunOnceInitializer(() -> { - runtimeImage[0] = JPackageCommand.createInputRuntimeImage(); + if (runtimeBundle) { + // Use custom plist file with the input runtime bundle. + runtimeImage.set(MacHelper.createRuntimeBundle(toConsumer(buildRuntimeBundleCmd -> { + // Use the same name for the input runtime bundle as the name of the output bundle. + // This is to make the plist file validation pass, as the custom plist file + // is configured for the command building the input runtime bundle, + // but the plist file from the output bundle is examined. + buildRuntimeBundleCmd.setDefaultAppName(); + cfg.init(buildRuntimeBundleCmd); + }))); + } else { + runtimeImage.set(JPackageCommand.createInputRuntimeImage()); + } }).addInitializer(cmd -> { cmd.ignoreDefaultRuntime(true) .removeArgumentWithValue("--input") - .setArgumentValue("--runtime-image", runtimeImage[0]); - cfg.init(cmd); + .setArgumentValue("--runtime-image", runtimeImage.get()); + if (!runtimeBundle) { + cfg.init(cmd); + } }).addInstallVerifier(cmd -> { cfg.createPListFilesVerifier(cmd).accept(cmd); }).run(Action.CREATE_AND_UNPACK); @@ -128,6 +161,31 @@ public class CustomInfoPListTest { }).toList(); } + private void testApp(ConfigurationTarget target, TestConfig cfg) { + + List> verifier = new ArrayList<>(); + + target.addInitializer(JPackageCommand::setFakeRuntime); + + target.addInitializer(toConsumer(cfg::init)); + + target.addRunOnceInitializer(_ -> { + verifier.add(cfg.createPListFilesVerifier( + target.cmd().orElseGet(JPackageCommand::helloAppImage).executePrerequisiteActions() + )); + }); + + target.cmd().ifPresent(JPackageCommand::executeAndAssertHelloAppImageCreated); + + target.addInstallVerifier(cmd -> { + verifier.get(0).accept(cmd); + }); + + target.test().ifPresent(test -> { + test.run(Action.CREATE_AND_UNPACK); + }); + } + 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()); @@ -171,27 +229,37 @@ public class CustomInfoPListTest { return cmd; } - ThrowingConsumer createPListFilesVerifier(JPackageCommand cmd) throws IOException { - ThrowingConsumer defaultVerifier = otherCmd -> { + Consumer createPListFilesVerifier(JPackageCommand cmd) { + Consumer customPListFilesVerifier = toConsumer(otherCmd -> { for (var customPList : customPLists) { customPList.verifyPListFile(otherCmd); } - }; + }); + // Get the list of default plist files. + // These are the plist files created from the plist file templates in jpackage resources. var defaultPListFiles = CustomPListType.defaultRoles(customPLists); if (defaultPListFiles.isEmpty()) { - return defaultVerifier; + // All plist files in the bundle are customized. + return customPListFilesVerifier; } else { - var vanillaCmd = new JPackageCommand().setFakeRuntime() + // There are some default plist files in the bundle. + // Verify the expected default plist files are such. + + // Create a copy of the `cmd` without the resource directory and with the app image bundling type. + // Execute it and get the default plist files. + var vanillaCmd = new JPackageCommand() .addArguments(cmd.getAllArguments()) .setPackageType(PackageType.IMAGE) .removeArgumentWithValue("--resource-dir") .setArgumentValue("--dest", TKit.createTempDirectory("vanilla")); - vanillaCmd.executeIgnoreExitCode().assertExitCodeIsZero(); + vanillaCmd.execute(); return otherCmd -> { - defaultVerifier.accept(otherCmd); + // Verify custom plist files. + customPListFilesVerifier.accept(otherCmd); + // Verify default plist files. for (var defaultPListFile : defaultPListFiles) { final var expectedPListPath = defaultPListFile.path(vanillaCmd); final var expectedPList = MacHelper.readPList(expectedPListPath); @@ -234,7 +302,10 @@ public class CustomInfoPListTest { CustomPListFactory.PLIST_OUTPUT::writeAppPlist, "Info.plist"), - APP_WITH_FA(APP), + APP_WITH_FA( + CustomPListFactory.PLIST_INPUT::writeAppPlistWithFa, + CustomPListFactory.PLIST_OUTPUT::writeAppPlistWithFa, + "Info.plist"), EMBEDDED_RUNTIME( CustomPListFactory.PLIST_INPUT::writeEmbeddedRuntimePlist, @@ -256,12 +327,6 @@ public class CustomInfoPListTest { 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); @@ -313,7 +378,15 @@ public class CustomInfoPListTest { PLIST_OUTPUT, ; + private void writeAppPlistWithFa(JPackageCommand cmd, XMLStreamWriter xml) throws XMLStreamException, IOException { + writeAppPlist(cmd, xml, true); + } + private void writeAppPlist(JPackageCommand cmd, XMLStreamWriter xml) throws XMLStreamException, IOException { + writeAppPlist(cmd, xml, false); + } + + private void writeAppPlist(JPackageCommand cmd, XMLStreamWriter xml, boolean withFa) throws XMLStreamException, IOException { writePList(xml, toXmlConsumer(() -> { writeDict(xml, toXmlConsumer(() -> { writeString(xml, "CustomAppProperty", "App"); @@ -326,7 +399,7 @@ public class CustomInfoPListTest { 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 (withFa) { if (this == PLIST_INPUT) { xml.writeCharacters("DEPLOY_FILE_ASSOCIATIONS"); } else { diff --git a/test/jdk/tools/jpackage/macosx/SigningRuntimeImagePackageTest.java b/test/jdk/tools/jpackage/macosx/SigningRuntimeImagePackageTest.java index 856962d3b01..ccc39f7a367 100644 --- a/test/jdk/tools/jpackage/macosx/SigningRuntimeImagePackageTest.java +++ b/test/jdk/tools/jpackage/macosx/SigningRuntimeImagePackageTest.java @@ -23,19 +23,15 @@ import static jdk.jpackage.internal.util.function.ThrowingConsumer.toConsumer; -import java.io.IOException; 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; import jdk.jpackage.test.JPackageCommand; import jdk.jpackage.test.MacHelper; import jdk.jpackage.test.MacSign; import jdk.jpackage.test.PackageTest; -import jdk.jpackage.test.PackageType; -import jdk.jpackage.test.TKit; /** * Tests generation of dmg and pkg with --mac-sign and related arguments. Test @@ -97,37 +93,10 @@ public class SigningRuntimeImagePackageTest { return cmd; } - private static Path createInputRuntimeBundle(MacSign.ResolvedKeychain keychain, int certIndex) throws IOException { - - final var runtimeImage = JPackageCommand.createInputRuntimeImage(); - - final var runtimeBundleWorkDir = TKit.createTempDirectory("runtime-bundle"); - - final var unpackadeRuntimeBundleDir = runtimeBundleWorkDir.resolve("unpacked"); - - var cmd = new JPackageCommand() - .useToolProvider(true) - .ignoreDefaultRuntime(true) - .dumpOutput(true) - .setPackageType(PackageType.MAC_DMG) - .setArgumentValue("--name", "foo") - .addArguments("--runtime-image", runtimeImage) - .addArguments("--dest", runtimeBundleWorkDir); - - addSignOptions(cmd, keychain, certIndex); - - cmd.execute(); - - MacHelper.withExplodedDmg(cmd, dmgImage -> { - if (dmgImage.endsWith(cmd.appInstallationDirectory().getFileName())) { - Executor.of("cp", "-R") - .addArgument(dmgImage) - .addArgument(unpackadeRuntimeBundleDir) - .execute(0); - } + private static Path createInputRuntimeBundle(MacSign.ResolvedKeychain keychain, int certIndex) { + return MacHelper.createRuntimeBundle(cmd -> { + addSignOptions(cmd, keychain, certIndex); }); - - return unpackadeRuntimeBundleDir; } @Test diff --git a/test/jdk/tools/jpackage/share/RuntimePackageTest.java b/test/jdk/tools/jpackage/share/RuntimePackageTest.java index caa129713b4..387b46acdfb 100644 --- a/test/jdk/tools/jpackage/share/RuntimePackageTest.java +++ b/test/jdk/tools/jpackage/share/RuntimePackageTest.java @@ -26,7 +26,6 @@ import static jdk.internal.util.OperatingSystem.MACOS; import static jdk.jpackage.test.TKit.assertFalse; import static jdk.jpackage.test.TKit.assertTrue; -import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Objects; @@ -34,9 +33,7 @@ import java.util.function.Predicate; import jdk.jpackage.internal.util.function.ThrowingSupplier; import jdk.jpackage.test.Annotations.Parameter; 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.LinuxHelper; import jdk.jpackage.test.MacHelper; import jdk.jpackage.test.PackageTest; @@ -89,7 +86,7 @@ public class RuntimePackageTest { @Test(ifOS = MACOS) public static void testFromBundle() { - init(RuntimePackageTest::createInputRuntimeBundle).run(); + init(MacHelper::createRuntimeBundle).run(); } @Test(ifOS = LINUX) @@ -114,7 +111,7 @@ public class RuntimePackageTest { } private static PackageTest init() { - return init(RuntimePackageTest::createInputRuntimeImage); + return init(JPackageCommand::createInputRuntimeImage); } private static PackageTest init(ThrowingSupplier createRuntime) { @@ -168,58 +165,4 @@ public class RuntimePackageTest { } 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 runtimeBundleWorkDir = TKit.createTempDirectory("runtime-bundle"); - - final var unpackadeRuntimeBundleDir = runtimeBundleWorkDir.resolve("unpacked"); - - var cmd = new JPackageCommand() - .useToolProvider(true) - .ignoreDefaultRuntime(true) - .dumpOutput(true) - .setPackageType(PackageType.MAC_DMG) - .setArgumentValue("--name", "foo") - .addArguments("--runtime-image", runtimeImage) - .addArguments("--dest", runtimeBundleWorkDir); - - cmd.execute(); - - MacHelper.withExplodedDmg(cmd, dmgImage -> { - if (dmgImage.endsWith(cmd.appInstallationDirectory().getFileName())) { - Executor.of("cp", "-R") - .addArgument(dmgImage) - .addArgument(unpackadeRuntimeBundleDir) - .execute(0); - } - }); - - return unpackadeRuntimeBundleDir; - } }