src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacFromOptions.java (229 lines of code) (raw):

/* * 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 static jdk.jpackage.internal.FromOptions.buildApplicationBuilder; import static jdk.jpackage.internal.FromOptions.createPackageBuilder; import static jdk.jpackage.internal.MacPackagingPipeline.APPLICATION_LAYOUT; import static jdk.jpackage.internal.MacRuntimeValidator.validateRuntimeHasJliLib; import static jdk.jpackage.internal.MacRuntimeValidator.validateRuntimeHasNoBinDir; import static jdk.jpackage.internal.cli.StandardBundlingOperation.SIGN_MAC_APP_IMAGE; import static jdk.jpackage.internal.cli.StandardOption.APPCLASS; import static jdk.jpackage.internal.cli.StandardOption.ICON; import static jdk.jpackage.internal.cli.StandardOption.MAC_APP_CATEGORY; import static jdk.jpackage.internal.cli.StandardOption.MAC_APP_IMAGE_SIGN_IDENTITY; import static jdk.jpackage.internal.cli.StandardOption.MAC_APP_STORE; import static jdk.jpackage.internal.cli.StandardOption.MAC_BUNDLE_IDENTIFIER; import static jdk.jpackage.internal.cli.StandardOption.MAC_BUNDLE_NAME; import static jdk.jpackage.internal.cli.StandardOption.MAC_BUNDLE_SIGNING_PREFIX; import static jdk.jpackage.internal.cli.StandardOption.MAC_DMG_CONTENT; import static jdk.jpackage.internal.cli.StandardOption.MAC_ENTITLEMENTS; import static jdk.jpackage.internal.cli.StandardOption.MAC_INSTALLER_SIGN_IDENTITY; import static jdk.jpackage.internal.cli.StandardOption.MAC_SIGN; import static jdk.jpackage.internal.cli.StandardOption.MAC_SIGNING_KEYCHAIN; import static jdk.jpackage.internal.cli.StandardOption.MAC_SIGNING_KEY_NAME; import static jdk.jpackage.internal.cli.StandardOption.PREDEFINED_APP_IMAGE; import static jdk.jpackage.internal.cli.StandardOption.PREDEFINED_RUNTIME_IMAGE; import static jdk.jpackage.internal.model.MacPackage.RUNTIME_BUNDLE_LAYOUT; import static jdk.jpackage.internal.model.StandardPackageType.MAC_DMG; import static jdk.jpackage.internal.model.StandardPackageType.MAC_PKG; import static jdk.jpackage.internal.util.function.ExceptionBox.toUnchecked; import java.nio.file.Path; import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.Optional; import jdk.jpackage.internal.ApplicationBuilder.MainLauncherStartupInfo; import jdk.jpackage.internal.SigningIdentityBuilder.ExpiredCertificateException; import jdk.jpackage.internal.SigningIdentityBuilder.StandardCertificateSelector; import jdk.jpackage.internal.cli.OptionValue; import jdk.jpackage.internal.cli.Options; import jdk.jpackage.internal.cli.StandardFaOption; import jdk.jpackage.internal.model.ApplicationLaunchers; import jdk.jpackage.internal.model.ExternalApplication; import jdk.jpackage.internal.model.FileAssociation; import jdk.jpackage.internal.model.Launcher; import jdk.jpackage.internal.model.MacApplication; import jdk.jpackage.internal.model.MacDmgPackage; import jdk.jpackage.internal.model.MacFileAssociation; import jdk.jpackage.internal.model.MacLauncher; import jdk.jpackage.internal.model.MacPackage; import jdk.jpackage.internal.model.MacPkgPackage; import jdk.jpackage.internal.model.PackageType; import jdk.jpackage.internal.model.RuntimeLayout; import jdk.jpackage.internal.util.MacBundle; import jdk.jpackage.internal.util.Result; import jdk.jpackage.internal.util.RootedPath; import jdk.jpackage.internal.util.function.ExceptionBox; final class MacFromOptions { static MacApplication createMacApplication(Options options) { return createMacApplicationInternal(options).app(); } static MacDmgPackage createMacDmgPackage(Options options) { final var app = createMacApplicationInternal(options); final var superPkgBuilder = createMacPackageBuilder(options, app, MAC_DMG); final var pkgBuilder = new MacDmgPackageBuilder(superPkgBuilder); MAC_DMG_CONTENT.findIn(options).map((List<Collection<RootedPath>> v) -> { // Reverse the order of content sources. // If there are multiple source files for the same // destination file, only the first will be used. // Reversing the order of content sources makes it use the last file // from the original list of source files for the given destination file. return v.reversed().stream().flatMap(Collection::stream).toList(); }).ifPresent(pkgBuilder::dmgRootDirSources); return pkgBuilder.create(); } static MacPkgPackage createMacPkgPackage(Options options) { // // One of "MacSignTest.testExpiredCertificate" test cases expects // two error messages about expired certificates in the output: one for // certificate for signing an app image, another certificate for signing a PKG. // So creation of a PKG package is a bit messy. // final boolean sign = MAC_SIGN.findIn(options).orElse(false); final boolean appStore = MAC_APP_STORE.findIn(options).orElse(false); final var appResult = Result.of(() -> createMacApplicationInternal(options)); final Optional<MacPkgPackageBuilder> pkgBuilder; if (appResult.hasValue()) { final var superPkgBuilder = createMacPackageBuilder(options, appResult.orElseThrow(), MAC_PKG); pkgBuilder = Optional.of(new MacPkgPackageBuilder(superPkgBuilder)); } else { // Failed to create an app. Is it because of the expired certificate? rethrowIfNotExpiredCertificateException(appResult); // Yes, the certificate for signing the app image has expired. // Keep going, try to create a signing config for the package. pkgBuilder = Optional.empty(); } if (sign) { final var signingIdentityBuilder = createSigningIdentityBuilder(options); MAC_INSTALLER_SIGN_IDENTITY.ifPresentIn(options, signingIdentityBuilder::signingIdentity); MAC_SIGNING_KEY_NAME.findIn(options).ifPresent(userName -> { final StandardCertificateSelector domain; if (appStore) { domain = StandardCertificateSelector.APP_STORE_PKG_INSTALLER; } else { domain = StandardCertificateSelector.PKG_INSTALLER; } signingIdentityBuilder.certificateSelector(StandardCertificateSelector.create(userName, domain)); }); if (pkgBuilder.isPresent()) { pkgBuilder.orElseThrow().signingBuilder(signingIdentityBuilder); } else { // // The certificate for signing the app image has expired. Can not create a // package because there is no app. // Try to create a signing config for the package and see if the certificate for // signing the package is also expired. // final var expiredAppCertException = appResult.firstError().orElseThrow(); final var pkgSignConfigResult = Result.of(signingIdentityBuilder::create); try { rethrowIfNotExpiredCertificateException(pkgSignConfigResult); // The certificate for the package signing config is also expired! } catch (RuntimeException ex) { // Some error occurred trying to configure the signing config for the package. // Ignore it, bail out with the first error. throw toUnchecked(expiredAppCertException); } Log.error(pkgSignConfigResult.firstError().orElseThrow().getMessage()); throw toUnchecked(expiredAppCertException); } } return pkgBuilder.orElseThrow().create(); } private record ApplicationWithDetails(MacApplication app, Optional<ExternalApplication> externalApp) { ApplicationWithDetails { Objects.requireNonNull(app); Objects.requireNonNull(externalApp); } } private static ApplicationWithDetails createMacApplicationInternal(Options options) { final var predefinedRuntimeLayout = PREDEFINED_RUNTIME_IMAGE.findIn(options) .map(MacPackage::guessRuntimeLayout); predefinedRuntimeLayout.ifPresent(layout -> { validateRuntimeHasJliLib(layout); if (MAC_APP_STORE.containsIn(options)) { validateRuntimeHasNoBinDir(layout); } }); final var launcherFromOptions = new LauncherFromOptions().faMapper(MacFromOptions::createMacFa); final var superAppBuilder = buildApplicationBuilder() .runtimeLayout(RUNTIME_BUNDLE_LAYOUT) .predefinedRuntimeLayout(predefinedRuntimeLayout.map(RuntimeLayout::unresolve).orElse(null)) .create(options, launcherOptions -> { var launcher = launcherFromOptions.create(launcherOptions); return MacLauncher.create(launcher); }, (MacLauncher _, Launcher launcher) -> { return MacLauncher.create(launcher); }, APPLICATION_LAYOUT); if (PREDEFINED_APP_IMAGE.containsIn(options)) { // Set the main launcher start up info. // AppImageFile assumes the main launcher start up info is available when // it is constructed from Application instance. // This happens when jpackage signs predefined app image. final var appImageFileOptions = superAppBuilder.externalApplication().orElseThrow().extra(); final var mainLauncherStartupInfo = new MainLauncherStartupInfo(APPCLASS.getFrom(appImageFileOptions)); final var launchers = superAppBuilder.launchers().orElseThrow(); final var mainLauncher = ApplicationBuilder.overrideLauncherStartupInfo(launchers.mainLauncher(), mainLauncherStartupInfo); superAppBuilder.launchers(new ApplicationLaunchers(MacLauncher.create(mainLauncher), launchers.additionalLaunchers())); } final var app = superAppBuilder.create(); final var appBuilder = new MacApplicationBuilder(app); 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); MAC_BUNDLE_IDENTIFIER.ifPresentIn(options, appBuilder::bundleIdentifier); MAC_APP_CATEGORY.ifPresentIn(options, appBuilder::category); final boolean sign; final boolean appStore; if (PREDEFINED_APP_IMAGE.containsIn(options) && OptionUtils.bundlingOperation(options) != SIGN_MAC_APP_IMAGE) { final var appImageFileOptions = superAppBuilder.externalApplication().orElseThrow().extra(); sign = MAC_SIGN.getFrom(appImageFileOptions); appStore = MAC_APP_STORE.getFrom(appImageFileOptions); } else { sign = MAC_SIGN.getFrom(options); appStore = MAC_APP_STORE.getFrom(options); } appBuilder.appStore(appStore); if (sign) { final var signingIdentityBuilder = createSigningIdentityBuilder(options); MAC_APP_IMAGE_SIGN_IDENTITY.ifPresentIn(options, signingIdentityBuilder::signingIdentity); MAC_SIGNING_KEY_NAME.findIn(options).ifPresent(userName -> { final StandardCertificateSelector domain; if (appStore) { domain = StandardCertificateSelector.APP_STORE_APP_IMAGE; } else { domain = StandardCertificateSelector.APP_IMAGE; } signingIdentityBuilder.certificateSelector(StandardCertificateSelector.create(userName, domain)); }); final var signingBuilder = new AppImageSigningConfigBuilder(signingIdentityBuilder); if (appStore) { signingBuilder.entitlementsResourceName("sandbox.plist"); } app.mainLauncher().flatMap(Launcher::startupInfo).ifPresentOrElse( signingBuilder::signingIdentifierPrefix, () -> { // Runtime installer does not have the main launcher, use // 'bundleIdentifier' as the prefix by default. var bundleIdentifier = appBuilder.create().bundleIdentifier(); signingBuilder.signingIdentifierPrefix(bundleIdentifier + "."); }); MAC_BUNDLE_SIGNING_PREFIX.ifPresentIn(options, signingBuilder::signingIdentifierPrefix); MAC_ENTITLEMENTS.ifPresentIn(options, signingBuilder::entitlements); appBuilder.signingBuilder(signingBuilder); } return new ApplicationWithDetails(appBuilder.create(), superAppBuilder.externalApplication()); } private static MacPackageBuilder createMacPackageBuilder(Options options, ApplicationWithDetails app, PackageType type) { final var builder = new MacPackageBuilder(createPackageBuilder(options, app.app(), type)); for (OptionValue<Path> ov : List.of(PREDEFINED_APP_IMAGE, PREDEFINED_RUNTIME_IMAGE)) { ov.findIn(options) .flatMap(MacBundle::fromPath) .map(MacPackagingPipeline::isSigned) .ifPresent(builder::predefinedAppImageSigned); } return builder; } private static void rethrowIfNotExpiredCertificateException(Result<?> result) { final var ex = result.firstError().orElseThrow(); if (ex instanceof ExpiredCertificateException) { return; } if (ex instanceof ExceptionBox box) { if (box.getCause() instanceof Exception cause) { rethrowIfNotExpiredCertificateException(Result.ofError(cause)); } } throw toUnchecked(ex); } private static SigningIdentityBuilder createSigningIdentityBuilder(Options options) { final var builder = new SigningIdentityBuilder(); MAC_SIGNING_KEYCHAIN.findIn(options).map(Path::toString).ifPresent(builder::keychain); return builder; } private static MacFileAssociation createMacFa(Options options, FileAssociation fa) { final var builder = new MacFileAssociationBuilder(); StandardFaOption.MAC_CFBUNDLETYPEROLE.ifPresentIn(options, builder::cfBundleTypeRole); StandardFaOption.MAC_LSHANDLERRANK.ifPresentIn(options, builder::lsHandlerRank); StandardFaOption.MAC_NSSTORETYPEKEY.ifPresentIn(options, builder::nsPersistentStoreTypeKey); StandardFaOption.MAC_NSDOCUMENTCLASS.ifPresentIn(options, builder::nsDocumentClass); StandardFaOption.MAC_LSTYPEISPACKAGE.ifPresentIn(options, builder::lsTypeIsPackage); StandardFaOption.MAC_LSDOCINPLACE.ifPresentIn(options, builder::lsSupportsOpeningDocumentsInPlace); StandardFaOption.MAC_UIDOCBROWSER.ifPresentIn(options, builder::uiSupportsDocumentBrowser); StandardFaOption.MAC_NSEXPORTABLETYPES.ifPresentIn(options, builder::nsExportableTypes); StandardFaOption.MAC_UTTYPECONFORMSTO.ifPresentIn(options, builder::utTypeConformsTo); return builder.create(fa); } }