8379426: [macos] jpackage: runtime bundle version suffix is out of sync with the version property in the Info.plist file

Reviewed-by: almatvee
This commit is contained in:
Alexey Semenyuk 2026-03-09 22:54:52 +00:00
parent 6a8e9530fe
commit 6a061f9d25
14 changed files with 582 additions and 211 deletions

View File

@ -0,0 +1,144 @@
/*
* Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jdk.internal.util.OSVersion;
final class ActiveKeychainList implements Closeable {
static Optional<ActiveKeychainList> createForPlatform(List<Keychain> keychains) throws IOException {
if (!keychains.isEmpty() && Globals.instance().findBooleanProperty(ActiveKeychainList.class).orElseGet(ActiveKeychainList::isRequired)) {
return Optional.of(new ActiveKeychainList(keychains));
} else {
return Optional.empty();
}
}
static Optional<ActiveKeychainList> createForPlatform(Keychain... keychains) throws IOException {
return createForPlatform(List.of(keychains));
}
@SuppressWarnings("try")
static void withKeychains(Consumer<List<Keychain>> keychainConsumer, List<Keychain> keychains) throws IOException {
var keychainList = createForPlatform(keychains);
if (keychainList.isEmpty()) {
keychainConsumer.accept(keychains);
} else {
try (var kl = keychainList.get()) {
keychainConsumer.accept(keychains);
}
}
}
static void withKeychain(Consumer<Keychain> keychainConsumer, Keychain keychain) throws IOException {
Objects.requireNonNull(keychainConsumer);
withKeychains(keychains -> {
keychainConsumer.accept(keychains.getFirst());
}, List.of(keychain));
}
ActiveKeychainList(List<Keychain> requestedKeychains, List<Keychain> currentKeychains, boolean force) throws IOException {
this.requestedKeychains = List.copyOf(requestedKeychains);
this.oldKeychains = List.copyOf(currentKeychains);
final List<String> cmdline = new ArrayList<>(LIST_KEYCHAINS_CMD_PREFIX);
addKeychains(cmdline, oldKeychains);
if (force) {
this.currentKeychains = requestedKeychains;
restoreKeychainsCmd = List.copyOf(cmdline);
cmdline.subList(LIST_KEYCHAINS_CMD_PREFIX.size(), cmdline.size()).clear();
addKeychains(cmdline, requestedKeychains);
} else {
final var currentKeychainPaths = oldKeychains.stream().map(Keychain::path).toList();
final var missingKeychains = requestedKeychains.stream().filter(k -> {
return !currentKeychainPaths.contains(k.path());
}).toList();
if (missingKeychains.isEmpty()) {
this.currentKeychains = oldKeychains;
restoreKeychainsCmd = List.of();
} else {
this.currentKeychains = Stream.of(oldKeychains, missingKeychains)
.flatMap(List::stream).collect(Collectors.toUnmodifiableList());
restoreKeychainsCmd = List.copyOf(cmdline);
addKeychains(cmdline, missingKeychains);
}
}
Executor.of(cmdline).executeExpectSuccess();
}
ActiveKeychainList(List<Keychain> keychains) throws IOException {
this(keychains, Keychain.listKeychains(), false);
}
List<Keychain> requestedKeychains() {
return requestedKeychains;
}
List<Keychain> currentKeychains() {
return currentKeychains;
}
List<Keychain> restoreKeychains() {
return oldKeychains;
}
@Override
public void close() throws IOException {
if (!restoreKeychainsCmd.isEmpty()) {
Executor.of(restoreKeychainsCmd).executeExpectSuccess();
}
}
private static void addKeychains(List<String> cmdline, List<Keychain> keychains) {
cmdline.addAll(keychains.stream().map(Keychain::asCliArg).toList());
}
private static boolean isRequired() {
// Required for OS X 10.12+
return 0 <= OSVersion.current().compareTo(new OSVersion(10, 12));
}
private final List<Keychain> requestedKeychains;
private final List<Keychain> currentKeychains;
private final List<Keychain> oldKeychains;
private final List<String> restoreKeychainsCmd;
private final static List<String> LIST_KEYCHAINS_CMD_PREFIX = List.of("/usr/bin/security", "list-keychains", "-s");
}

View File

@ -1,65 +0,0 @@
/*
* Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import jdk.jpackage.internal.model.DottedVersion;
import jdk.jpackage.internal.util.PListReader;
import org.xml.sax.SAXException;
/**
* Mandatory elements of Info.plist file of app image.
*/
record AppImageInfoPListFile(String bundleIdentifier, String bundleName, String copyright,
DottedVersion shortVersion, DottedVersion bundleVersion, String category) {
static final class InvalidPlistFileException extends Exception {
InvalidPlistFileException(Throwable cause) {
super(cause);
}
private static final long serialVersionUID = 1L;
}
static AppImageInfoPListFile loadFromInfoPList(Path infoPListFile)
throws IOException, InvalidPlistFileException, SAXException {
final var plistReader = new PListReader(Files.readAllBytes(infoPListFile));
try {
return new AppImageInfoPListFile(
plistReader.queryValue("CFBundleIdentifier"),
plistReader.queryValue("CFBundleName"),
plistReader.queryValue("NSHumanReadableCopyright"),
DottedVersion.greedy(plistReader.queryValue("CFBundleShortVersionString")),
DottedVersion.greedy(plistReader.queryValue("CFBundleVersion")),
plistReader.queryValue("LSApplicationCategoryType"));
} catch (Exception ex) {
throw new InvalidPlistFileException(ex);
}
}
}

View File

@ -36,20 +36,24 @@ import java.util.stream.Stream;
import jdk.jpackage.internal.model.AppImageLayout;
import jdk.jpackage.internal.model.AppImageSigningConfig;
import jdk.jpackage.internal.model.Application;
import jdk.jpackage.internal.model.ApplicationLaunchers;
import jdk.jpackage.internal.model.ExternalApplication;
import jdk.jpackage.internal.model.JPackageException;
import jdk.jpackage.internal.model.Launcher;
import jdk.jpackage.internal.model.MacApplication;
import jdk.jpackage.internal.model.MacApplicationMixin;
import jdk.jpackage.internal.model.JPackageException;
import jdk.jpackage.internal.util.PListReader;
import jdk.jpackage.internal.util.Result;
import jdk.jpackage.internal.util.RootedPath;
final class MacApplicationBuilder {
MacApplicationBuilder(Application app) {
this.app = Objects.requireNonNull(app);
MacApplicationBuilder(ApplicationBuilder appBuilder) {
this.superBuilder = Objects.requireNonNull(appBuilder);
}
private MacApplicationBuilder(MacApplicationBuilder other) {
this(other.app);
this(other.superBuilder.copy());
icon = other.icon;
bundleName = other.bundleName;
bundleIdentifier = other.bundleIdentifier;
@ -94,18 +98,28 @@ final class MacApplicationBuilder {
return this;
}
Optional<ExternalApplication> externalApplication() {
return superBuilder.externalApplication();
}
Optional<ApplicationLaunchers> launchers() {
return superBuilder.launchers();
}
MacApplication create() {
if (externalInfoPlistFile != null) {
return createCopyForExternalInfoPlistFile().create();
}
var app = superBuilder.create();
validateAppVersion(app);
validateAppContentDirs(app);
final var mixin = new MacApplicationMixin.Stub(
validatedIcon(),
validatedBundleName(),
validatedBundleIdentifier(),
validatedBundleName(app),
validatedBundleIdentifier(app),
validatedCategory(),
appStore,
createSigningConfig());
@ -161,39 +175,58 @@ final class MacApplicationBuilder {
}
private MacApplicationBuilder createCopyForExternalInfoPlistFile() {
try {
final var plistFile = AppImageInfoPListFile.loadFromInfoPList(externalInfoPlistFile);
final var builder = new MacApplicationBuilder(this);
final var builder = new MacApplicationBuilder(this);
builder.externalInfoPlistFile(null);
builder.externalInfoPlistFile(null);
Result<PListReader> plistResult = Result.of(() -> {
return new PListReader(Files.readAllBytes(externalInfoPlistFile));
}, Exception.class);
plistResult.value().ifPresent(plist -> {
if (builder.bundleName == null) {
builder.bundleName(plistFile.bundleName());
plist.findValue("CFBundleName").ifPresent(builder::bundleName);
}
if (builder.bundleIdentifier == null) {
builder.bundleIdentifier(plistFile.bundleIdentifier());
plist.findValue("CFBundleIdentifier").ifPresent(builder::bundleIdentifier);
}
if (builder.category == null) {
builder.category(plistFile.category());
plist.findValue("LSApplicationCategoryType").ifPresent(builder::category);
}
return builder;
} catch (IOException ex) {
throw new UncheckedIOException(ex);
} catch (Exception ex) {
throw new JPackageException(
I18N.format("error.invalid-app-image-plist-file", externalInfoPlistFile), ex);
}
if (builder.superBuilder.version().isEmpty()) {
plist.findValue("CFBundleVersion").ifPresent(builder.superBuilder::version);
}
});
plistResult.firstError().filter(_ -> {
// If we are building a runtime and the Info.plist file of the predefined
// runtime bundle is malformed or unavailable, ignore it.
return !superBuilder.isRuntime();
}).ifPresent(ex -> {
// We are building an application from the predefined app image and
// the Info.plist file in the predefined app image bundle is malformed or unavailable. Bail out.
switch (ex) {
case IOException ioex -> {
throw new UncheckedIOException(ioex);
}
default -> {
throw new JPackageException(
I18N.format("error.invalid-app-image-plist-file", externalInfoPlistFile), ex);
}
}
});
return builder;
}
private Optional<AppImageSigningConfig> createSigningConfig() {
return Optional.ofNullable(signingBuilder).map(AppImageSigningConfigBuilder::create);
}
private String validatedBundleName() {
private String validatedBundleName(Application app) {
final var value = Optional.ofNullable(bundleName).orElseGet(() -> {
final var appName = app.name();
// Commented out for backward compatibility
@ -212,7 +245,7 @@ final class MacApplicationBuilder {
return value;
}
private String validatedBundleIdentifier() {
private String validatedBundleIdentifier(Application app) {
final var value = Optional.ofNullable(bundleIdentifier).orElseGet(() -> {
return app.mainLauncher()
.flatMap(Launcher::startupInfo)
@ -255,7 +288,7 @@ final class MacApplicationBuilder {
private Path externalInfoPlistFile;
private AppImageSigningConfigBuilder signingBuilder;
private final Application app;
private final ApplicationBuilder superBuilder;
private static final Defaults DEFAULTS = new Defaults("utilities");

View File

@ -198,7 +198,7 @@ final class MacFromOptions {
}
}
private static ApplicationWithDetails createMacApplicationInternal(Options options) {
private static ApplicationBuilder createApplicationBuilder(Options options) {
final var predefinedRuntimeLayout = PREDEFINED_RUNTIME_IMAGE.findIn(options)
.map(MacPackage::guessRuntimeLayout);
@ -236,12 +236,30 @@ final class MacFromOptions {
final var app = superAppBuilder.create();
final var appBuilder = new MacApplicationBuilder(app);
return superAppBuilder;
}
PREDEFINED_APP_IMAGE.findIn(options)
.map(MacBundle::new)
.map(MacBundle::infoPlistFile)
.ifPresent(appBuilder::externalInfoPlistFile);
private static ApplicationWithDetails createMacApplicationInternal(Options options) {
final var appBuilder = new MacApplicationBuilder(createApplicationBuilder(options));
if (OptionUtils.isRuntimeInstaller(options)) {
// Predefined runtime image, if specified, can be a macOS bundle or regular directory.
// Notify application builder with the path to the plist file in the predefined runtime image only if the file exists.
// If it doesn't, jpackage should keep going.
PREDEFINED_RUNTIME_IMAGE.findIn(options)
.flatMap(MacBundle::fromPath)
.map(MacBundle::infoPlistFile)
.ifPresent(appBuilder::externalInfoPlistFile);
} else {
// Predefined app image, if specified, should always be a valid macOS bundle.
// Notify application builder with the path to the plist file in the predefined app image without checking if the file exists.
// If it doesn't, the builder should throw and jpackage should exit with error.
PREDEFINED_APP_IMAGE.findIn(options)
.map(MacBundle::new)
.map(MacBundle::infoPlistFile)
.ifPresent(appBuilder::externalInfoPlistFile);
}
ICON.ifPresentIn(options, appBuilder::icon);
MAC_BUNDLE_NAME.ifPresentIn(options, appBuilder::bundleName);
@ -252,7 +270,7 @@ final class MacFromOptions {
final boolean appStore;
if (PREDEFINED_APP_IMAGE.containsIn(options)) {
final var appImageFileOptions = superAppBuilder.externalApplication().orElseThrow().extra();
final var appImageFileOptions = appBuilder.externalApplication().orElseThrow().extra();
appStore = MAC_APP_STORE.getFrom(appImageFileOptions);
} else {
appStore = MAC_APP_STORE.getFrom(options);
@ -295,7 +313,7 @@ final class MacFromOptions {
signingBuilder.entitlementsResourceName("sandbox.plist");
}
app.mainLauncher().flatMap(Launcher::startupInfo).ifPresentOrElse(
appBuilder.launchers().map(ApplicationLaunchers::mainLauncher).flatMap(Launcher::startupInfo).ifPresentOrElse(
signingBuilder::signingIdentifierPrefix,
() -> {
// Runtime installer does not have the main launcher, use
@ -310,7 +328,7 @@ final class MacFromOptions {
appBuilder.signingBuilder(signingBuilder);
}
return new ApplicationWithDetails(appBuilder.create(), superAppBuilder.externalApplication());
return new ApplicationWithDetails(appBuilder.create(), appBuilder.externalApplication());
}
private static MacPackageBuilder createMacPackageBuilder(Options options, ApplicationWithDetails app, PackageType type) {

View File

@ -500,7 +500,7 @@ final class MacPackagingPipeline {
};
app.signingConfig().flatMap(AppImageSigningConfig::keychain).map(Keychain::new).ifPresentOrElse(keychain -> {
toBiConsumer(TempKeychain::withKeychain).accept(unused -> signAction.run(), keychain);
toBiConsumer(ActiveKeychainList::withKeychain).accept(unused -> signAction.run(), keychain);
}, signAction);
}

View File

@ -1,103 +0,0 @@
/*
* Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal;
import java.io.Closeable;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import jdk.internal.util.OSVersion;
final class TempKeychain implements Closeable {
static void withKeychains(Consumer<List<Keychain>> keychainConsumer, List<Keychain> keychains) {
keychains.forEach(Objects::requireNonNull);
if (keychains.isEmpty() || OSVersion.current().compareTo(new OSVersion(10, 12)) < 0) {
keychainConsumer.accept(keychains);
} else {
// we need this for OS X 10.12+
try (var tempKeychain = new TempKeychain(keychains)) {
keychainConsumer.accept(tempKeychain.keychains);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
}
static void withKeychain(Consumer<Keychain> keychainConsumer, Keychain keychain) {
Objects.requireNonNull(keychainConsumer);
withKeychains(keychains -> {
keychainConsumer.accept(keychains.getFirst());
}, List.of(keychain));
}
TempKeychain(List<Keychain> keychains) throws IOException {
this.keychains = Objects.requireNonNull(keychains);
final var currentKeychains = Keychain.listKeychains();
final var currentKeychainPaths = currentKeychains.stream().map(Keychain::path).toList();
final var missingKeychains = keychains.stream().filter(k -> {
return !currentKeychainPaths.contains(k.path());
}).toList();
if (missingKeychains.isEmpty()) {
restoreKeychainsCmd = List.of();
} else {
List<String> args = new ArrayList<>();
args.add("/usr/bin/security");
args.add("list-keychains");
args.add("-s");
args.addAll(currentKeychains.stream().map(Keychain::asCliArg).toList());
restoreKeychainsCmd = List.copyOf(args);
args.addAll(missingKeychains.stream().map(Keychain::asCliArg).toList());
Executor.of(args).executeExpectSuccess();
}
}
List<Keychain> keychains() {
return keychains;
}
@Override
public void close() throws IOException {
if (!restoreKeychainsCmd.isEmpty()) {
Executor.of(restoreKeychainsCmd).executeExpectSuccess();
}
}
private final List<Keychain> keychains;
private final List<String> restoreKeychainsCmd;
}

View File

@ -45,10 +45,32 @@ import jdk.jpackage.internal.model.LauncherIcon;
import jdk.jpackage.internal.model.LauncherStartupInfo;
import jdk.jpackage.internal.model.ResourceDirLauncherIcon;
import jdk.jpackage.internal.model.RuntimeBuilder;
import jdk.jpackage.internal.model.RuntimeLayout;
import jdk.jpackage.internal.util.RootedPath;
final class ApplicationBuilder {
ApplicationBuilder() {
}
ApplicationBuilder(ApplicationBuilder other) {
name = other.name;
description = other.description;
version = other.version;
vendor = other.vendor;
copyright = other.copyright;
appDirSources = other.appDirSources;
externalApp = other.externalApp;
contentDirSources = other.contentDirSources;
appImageLayout = other.appImageLayout;
runtimeBuilder = other.runtimeBuilder;
launchers = other.launchers;
}
ApplicationBuilder copy() {
return new ApplicationBuilder(this);
}
Application create() {
Objects.requireNonNull(appImageLayout);
@ -103,6 +125,11 @@ final class ApplicationBuilder {
return this;
}
boolean isRuntime() {
return Optional.ofNullable(appImageLayout)
.orElseThrow(IllegalStateException::new) instanceof RuntimeLayout;
}
ApplicationBuilder name(String v) {
name = v;
return this;
@ -118,6 +145,10 @@ final class ApplicationBuilder {
return this;
}
Optional<String> version() {
return Optional.ofNullable(version);
}
ApplicationBuilder vendor(String v) {
vendor = v;
return this;

View File

@ -25,6 +25,9 @@
package jdk.jpackage.internal;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
@ -47,6 +50,21 @@ public final class Globals {
return objectFactory(ObjectFactory.build(objectFactory).executorFactory(v).create());
}
@SuppressWarnings("unchecked")
public <T> Optional<T> findProperty(Object key) {
return Optional.ofNullable((T)properties.get(Objects.requireNonNull(key)));
}
public Optional<Boolean> findBooleanProperty(Object key) {
return findProperty(key);
}
public <T> Globals setProperty(Object key, T value) {
checkMutable();
properties.compute(Objects.requireNonNull(key), (_, _) -> value);
return this;
}
Log.Logger logger() {
return logger;
}
@ -79,6 +97,7 @@ public final class Globals {
private ObjectFactory objectFactory = ObjectFactory.DEFAULT;
private final Log.Logger logger = new Log.Logger();
private final Map<Object, Object> properties = new HashMap<>();
private static final ScopedValue<Globals> INSTANCE = ScopedValue.newInstance();
private static final Globals DEFAULT = new Globals();

View File

@ -60,6 +60,7 @@ import java.util.spi.ToolProvider;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import jdk.jpackage.internal.util.MacBundle;
import jdk.jpackage.internal.util.function.ExceptionBox;
import jdk.jpackage.internal.util.function.ThrowingConsumer;
import jdk.jpackage.internal.util.function.ThrowingFunction;
@ -245,7 +246,18 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
public String version() {
return PropertyFinder.findAppProperty(this,
PropertyFinder.cmdlineOptionWithValue("--app-version"),
PropertyFinder.cmdlineOptionWithValue("--app-version").or(cmd -> {
if (cmd.isRuntime() && PackageType.MAC.contains(cmd.packageType())) {
// This is a macOS runtime bundle.
var predefinedRuntimeBundle = MacBundle.fromPath(Path.of(cmd.getArgumentValue("--runtime-image")));
if (predefinedRuntimeBundle.isPresent()) {
// This is a macOS runtime bundle created from the predefined runtime bundle (not a predefined runtime directory).
// The version of this bundle should be copied from the Info.plist file of the predefined runtime bundle.
return MacHelper.readPList(predefinedRuntimeBundle.get().infoPlistFile()).findValue("CFBundleVersion");
}
}
return Optional.empty();
}),
PropertyFinder.appImageFile(appImageFile -> {
return appImageFile.version();
})

View File

@ -0,0 +1,269 @@
/*
* Copyright (c) 2026, 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.internal;
import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import jdk.jpackage.test.mock.CommandAction;
import jdk.jpackage.test.mock.CommandActionSpec;
import jdk.jpackage.test.mock.CommandActionSpecs;
import jdk.jpackage.test.mock.CommandMockSpec;
import jdk.jpackage.test.mock.MockIllegalStateException;
import jdk.jpackage.test.mock.Script;
import jdk.jpackage.test.stdmock.JPackageMockUtils;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
class ActiveKeychainListTest {
@ParameterizedTest
@CsvSource(value = {
"'','',''",
"a,'',a",
"'',a,a",
"a,a,a",
"abc,b,abc",
"abc,ba,abc",
"abc,bad,abcd",
"ac,b,acb"
})
void test_ctor_and_createForPlatform(String initial, String requested, String current) throws IOException {
var initialKeychains = parseKeychainList(initial);
var requestedKeychains = parseKeychainList(requested);
var securityMock = new SecurityKeychainListMock(true);
initialKeychains.stream().map(Keychain::name).forEach(securityMock.keychainNames()::add);
Globals.main(toSupplier(() -> {
securityMock.applyToGlobals();
Globals.instance().setProperty(ActiveKeychainList.class, false);
assertTrue(ActiveKeychainList.createForPlatform(requestedKeychains.toArray(Keychain[]::new)).isEmpty());
var akl = new ActiveKeychainList(List.copyOf(requestedKeychains));
try (akl) {
assertEquals(initialKeychains, akl.restoreKeychains());
assertEquals(requestedKeychains, akl.requestedKeychains());
assertEquals(parseKeychainList(current), akl.currentKeychains());
assertEquals(akl.currentKeychains(), securityMock.keychains());
}
assertEquals(initialKeychains, akl.restoreKeychains());
assertEquals(requestedKeychains, akl.requestedKeychains());
assertEquals(parseKeychainList(current), akl.currentKeychains());
assertEquals(initialKeychains, securityMock.keychains());
return 0;
}));
}
@ParameterizedTest
@CsvSource(value = {
"'','','',true",
"'','','',false",
"a,'','',true",
"a,'',a,false",
"'',a,a,true",
"'',a,a,false",
"a,a,a,true",
"a,a,a,false",
"abc,b,b,true",
"abc,b,abc,false",
"abc,ba,ba,true",
"abc,ba,abc,false",
"abc,bad,bad,true",
"abc,bad,abcd,false",
"ac,b,b,true",
"ac,b,acb,false"
})
void testCtorWithForced(String initial, String requested, String current, boolean forced) throws IOException {
var initialKeychains = parseKeychainList(initial);
var requestedKeychains = parseKeychainList(requested);
var securityMock = new SecurityKeychainListMock(forced);
securityMock.keychainNames().addAll(List.of("foo", "bar"));
Globals.main(toSupplier(() -> {
securityMock.applyToGlobals();
var akl = new ActiveKeychainList(List.copyOf(requestedKeychains), List.copyOf(initialKeychains), forced);
try (akl) {
assertEquals(initialKeychains, akl.restoreKeychains());
assertEquals(requestedKeychains, akl.requestedKeychains());
assertEquals(parseKeychainList(current), akl.currentKeychains());
assertEquals(akl.currentKeychains(), securityMock.keychains());
}
assertEquals(initialKeychains, akl.restoreKeychains());
assertEquals(requestedKeychains, akl.requestedKeychains());
assertEquals(parseKeychainList(current), akl.currentKeychains());
assertEquals(initialKeychains, securityMock.keychains());
return 0;
}));
}
@ParameterizedTest
@CsvSource(value = {
"'','',",
"a,'',",
"'',a,a",
"a,a,a",
"abc,b,abc",
"abc,ba,abc",
"abc,bad,abcd",
"ac,b,acb"
})
void test_withKeychain(String initial, String requested, String current) throws IOException {
var initialKeychains = parseKeychainList(initial);
var requestedKeychains = parseKeychainList(requested);
for (boolean isRequired : List.of(true, false)) {
var securityMock = new SecurityKeychainListMock(true);
initialKeychains.stream().map(Keychain::name).forEach(securityMock.keychainNames()::add);
Consumer<List<Keychain>> workload = keychains -> {
assertEquals(requestedKeychains, keychains);
if (isRequired && current != null) {
assertEquals(parseKeychainList(current), securityMock.keychains());
} else {
assertEquals(initialKeychains, securityMock.keychains());
}
};
Globals.main(toSupplier(() -> {
Globals.instance().setProperty(ActiveKeychainList.class, isRequired);
securityMock.applyToGlobals();
ActiveKeychainList.withKeychains(workload, requestedKeychains);
assertEquals(initialKeychains, securityMock.keychains());
if (requestedKeychains.size() == 1) {
securityMock.applyToGlobals();
ActiveKeychainList.withKeychain(keychain -> {
workload.accept(List.of(keychain));
}, requestedKeychains.getFirst());
assertEquals(initialKeychains, securityMock.keychains());
}
return 0;
}));
}
}
/**
* Mocks "/usr/bin/security list-keychain" command.
*/
record SecurityKeychainListMock(List<String> keychainNames, boolean isReadAllowed) implements CommandAction {
SecurityKeychainListMock {
Objects.requireNonNull(keychainNames);
}
SecurityKeychainListMock(boolean isReadAllowed) {
this(new ArrayList<>(), isReadAllowed);
}
List<Keychain> keychains() {
return keychainNames.stream().map(Keychain::new).toList();
}
void applyToGlobals() {
CommandActionSpec actionSpec = CommandActionSpec.create("/usr/bin/security", this);
var script = Script.build()
.commandMockBuilderMutator(mockBuilder -> {
// Limit the number of times the mock can be executed.
// It should be one or twice.
// Once, when ActiveKeychainList is constructed such that it doesn't read
// the current active keychain list from the "/usr/bin/security" command, but takes it from the parameter.
// Twice, when ActiveKeychainList is constructed such that it read
// the current active keychain list from the "/usr/bin/security" command.
mockBuilder.repeat(isReadAllowed ? 2 : 1);
})
// Replace "/usr/bin/security" with the mock bound to the keychain mock.
.map(new CommandMockSpec(actionSpec.description(), "security-list-keychain", CommandActionSpecs.build().action(actionSpec).create()))
.createLoop();
JPackageMockUtils.buildJPackage()
.script(script)
.listener(System.out::println)
.applyToGlobals();
}
@Override
public Optional<Integer> run(Context context) throws Exception, MockIllegalStateException {
final var origContext = context;
if (!context.args().getFirst().equals("list-keychains")) {
throw origContext.unexpectedArguments();
}
context = context.shift();
if (context.args().isEmpty()) {
if (isReadAllowed) {
keychainNames.stream().map(k -> {
return new StringBuilder().append('"').append(k).append('"').toString();
}).forEach(context::printlnOut);
} else {
throw origContext.unexpectedArguments();
}
} else if (context.args().getFirst().equals("-s")) {
keychainNames.clear();
keychainNames.addAll(context.shift().args());
} else {
throw origContext.unexpectedArguments();
}
return Optional.of(0);
}
}
private static List<Keychain> parseKeychainList(String str) {
return str.chars().mapToObj(chr -> {
return new StringBuilder().append((char)chr).toString();
}).map(Keychain::new).toList();
}
}

View File

@ -175,13 +175,12 @@ public class MacDmgPackagerTest {
private static void runPackagingMock(Path workDir, MacDmgSystemEnvironment sysEnv) {
var app = new ApplicationBuilder()
var appBuilder = new ApplicationBuilder()
.appImageLayout(MacPackagingPipeline.APPLICATION_LAYOUT)
.runtimeBuilder(createRuntimeBuilder())
.name("foo")
.create();
.name("foo");
var macApp = new MacApplicationBuilder(app).create();
var macApp = new MacApplicationBuilder(appBuilder).create();
var macDmgPkg = new MacDmgPackageBuilder(new MacPackageBuilder(new PackageBuilder(macApp, MAC_DMG))).create();

View File

@ -52,3 +52,14 @@
* jdk/jpackage/internal/MacDmgPackagerTest.java
* @run junit jdk.jpackage/jdk.jpackage.internal.MacDmgPackagerTest
*/
/* @test
* @summary Test ActiveKeychainListTest
* @requires (os.family == "mac")
* @library /test/jdk/tools/jpackage/helpers
* @build jdk.jpackage.test.mock.*
* @build jdk.jpackage.test.stdmock.*
* @compile/module=jdk.jpackage -Xlint:all -Werror
* jdk/jpackage/internal/ActiveKeychainListTest.java
* @run junit jdk.jpackage/jdk.jpackage.internal.ActiveKeychainListTest
*/

View File

@ -140,11 +140,9 @@ public final class ErrorTest {
var appImageDir = (Path)APP_IMAGE.expand(cmd).orElseThrow();
// Replace the default Info.plist file with an empty one.
var plistFile = new MacBundle(appImageDir).infoPlistFile();
TKit.trace(String.format("Create invalid plist file in [%s]", plistFile));
TKit.trace(String.format("Create invalid plist file [%s]", plistFile));
createXml(plistFile, xml -> {
writePList(xml, toXmlConsumer(() -> {
writeDict(xml, toXmlConsumer(() -> {
}));
}));
});
return appImageDir;

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2018, 2026, 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
@ -86,7 +86,12 @@ public class RuntimePackageTest {
@Test(ifOS = MACOS)
public static void testFromBundle() {
init(MacHelper::createRuntimeBundle).run();
init(() -> {
return MacHelper.buildRuntimeBundle().mutator(cmd -> {
// Set custom version in the Info.plist file of the predefined runtime bundle.
cmd.addArguments("--app-version", "17.52");
}).create();
}).run();
}
@Test(ifOS = LINUX)