8371076: jpackage will wrongly overwrite the plist file in the embedded runtime when executed with the "--app-image" option

Reviewed-by: almatvee
This commit is contained in:
Alexey Semenyuk 2025-11-02 02:19:11 +00:00
parent f7f4f903cf
commit 7c900da198
13 changed files with 511 additions and 251 deletions

View File

@ -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)) {

View File

@ -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);
}

View File

@ -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<TaskID> taskGraph;
private final Map<TaskID, TaskConfig> taskConfig;
private final UnaryOperator<TaskContext> contextMapper;
private static final boolean TRACE_TASK_GRAPTH = false;
private static final boolean TRACE_TASK_ACTION = false;
}

View File

@ -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<String> 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<PListReader> 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<Boolean> 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<String> 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<List<String>> 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<Object> 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<Stream<Object>> 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<Node> 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());
}

View File

@ -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.
* <p>
* 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<String> messageConsumer) {
* callback = messageConsumer;
* return this;
* }
*
* void run() {
* for (;;) {
* var msg = fetchNextMessage();
* msg.ifPresent(callback);
* if (msg.isEmpty()) {
* break;
* }
* }
* }
*
* abstract Optional<String> fetchNextMessage();
*
* private Consumer<String> 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 <T> value type
*/
public final class Slot<T> {
public static <T> Slot<T> createEmpty() {
return new Slot<>();
}
public T get() {
return find().orElseThrow();
}
public Optional<T> find() {
return Optional.ofNullable(value);
}
public void set(T v) {
value = Objects.requireNonNull(v);
}
private T value;
}

View File

@ -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<Source> sources)
public static void concatXml(XMLStreamWriter xml, Source... sources) throws XMLStreamException, IOException {
concatXml(xml, List.of(sources));
}
public static void concatXml(XMLStreamWriter xml, Iterable<? extends Source> sources)
throws XMLStreamException, IOException {
xml = (XMLStreamWriter) Proxy.newProxyInstance(XMLStreamWriter.class.getClassLoader(),
new Class<?>[]{XMLStreamWriter.class},

View File

@ -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);

View File

@ -376,7 +376,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
return cmd;
}
public static Path createInputRuntimeImage() throws IOException {
public static Path createInputRuntimeImage() {
final Path runtimeImageDir;
@ -406,7 +406,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
}
public JPackageCommand setDefaultAppName() {
return addArguments("--name", TKit.getCurrentDefaultAppName());
return setArgumentValue("--name", TKit.getCurrentDefaultAppName());
}
/**

View File

@ -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<Properties, String> contentType = fa -> {
return String.format("%s.%s", bundleId, Objects.requireNonNull(fa.getProperty("extension")));
};
Function<Properties, Optional<String>> icon = fa -> {
return Optional.ofNullable(fa.getProperty("icon")).map(Path::of).map(Path::getFileName).map(Path::toString);
};
BiFunction<Properties, String, Optional<Boolean>> asBoolean = (fa, key) -> {
return Optional.ofNullable(fa.getProperty(key)).map(Boolean::parseBoolean);
};
BiFunction<Properties, String, List<String>> 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<Properties, String> contentType = fa -> {
return String.format("%s.%s", bundleId, Objects.requireNonNull(fa.getProperty("extension")));
};
Function<Properties, Optional<String>> icon = fa -> {
return Optional.ofNullable(fa.getProperty("icon")).map(Path::of).map(Path::getFileName).map(Path::toString);
};
BiFunction<Properties, String, Optional<Boolean>> asBoolean = (fa, key) -> {
return Optional.ofNullable(fa.getProperty(key)).map(Boolean::parseBoolean);
};
BiFunction<Properties, String, List<String>> 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<JPackageCommand> mutator) {
return createRuntimeBundle(Optional.of(mutator));
}
public static Path createRuntimeBundle() {
return createRuntimeBundle(Optional.empty());
}
public static Path createRuntimeBundle(Optional<Consumer<JPackageCommand>> 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<JPackageCommand> useKeychain(MacSign.ResolvedKeychain keychain) {

View File

@ -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<String, Object> xmlAsMap, String... xml) {
ParsedPList {
Objects.requireNonNull(xmlAsMap);
}
ParsedPList(Map<String, Object> xmlAsMap, List<String> xml) {
this(xmlAsMap, xml.toArray(String[]::new));
}
}
private static Stream<ParsedPList> parsedPLists() {
var xml = List.of(
"<key>AppName</key>",
"<string>Hello</string>",
"<!-- Application version -->",
@ -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<TestSpec> test() {

View File

@ -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<ThrowingConsumer<JPackageCommand>> 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.<Consumer<JPackageCommand>>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.<Path>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<Consumer<JPackageCommand>> 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<String> 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<JPackageCommand> createPListFilesVerifier(JPackageCommand cmd) throws IOException {
ThrowingConsumer<JPackageCommand> defaultVerifier = otherCmd -> {
Consumer<JPackageCommand> createPListFilesVerifier(JPackageCommand cmd) {
Consumer<JPackageCommand> 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 {

View File

@ -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

View File

@ -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<Path> 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;
}
}