mirror of
https://github.com/openjdk/jdk.git
synced 2026-05-19 18:07:49 +00:00
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:
parent
6a8e9530fe
commit
6a061f9d25
@ -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");
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
})
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user