| /* | |
| * 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; | |
| import static jdk.jpackage.internal.ApplicationImageUtils.createWriteAppImageFileAction; | |
| import static jdk.jpackage.internal.util.PListWriter.writeArray; | |
| import static jdk.jpackage.internal.util.PListWriter.writeBoolean; | |
| import static jdk.jpackage.internal.util.PListWriter.writeBooleanOptional; | |
| import static jdk.jpackage.internal.util.PListWriter.writeDict; | |
| 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.toXmlConsumer; | |
| import static jdk.jpackage.internal.util.function.ThrowingBiConsumer.toBiConsumer; | |
| import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier; | |
| import java.io.IOException; | |
| import java.io.StringWriter; | |
| import java.nio.charset.StandardCharsets; | |
| import java.nio.file.Files; | |
| import java.nio.file.Path; | |
| import java.util.ArrayList; | |
| import java.util.HashMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Objects; | |
| import java.util.Optional; | |
| import javax.xml.stream.XMLOutputFactory; | |
| import javax.xml.stream.XMLStreamException; | |
| import javax.xml.stream.XMLStreamWriter; | |
| import jdk.jpackage.internal.PackagingPipeline.AppImageBuildEnv; | |
| import jdk.jpackage.internal.PackagingPipeline.ApplicationImageTaskAction; | |
| import jdk.jpackage.internal.PackagingPipeline.BuildApplicationTaskID; | |
| import jdk.jpackage.internal.PackagingPipeline.CopyAppImageTaskID; | |
| import jdk.jpackage.internal.PackagingPipeline.PackageBuildEnv; | |
| import jdk.jpackage.internal.PackagingPipeline.PackageTaskID; | |
| import jdk.jpackage.internal.PackagingPipeline.PrimaryTaskID; | |
| import jdk.jpackage.internal.PackagingPipeline.TaskAction; | |
| import jdk.jpackage.internal.PackagingPipeline.TaskContext; | |
| import jdk.jpackage.internal.PackagingPipeline.TaskID; | |
| import jdk.jpackage.internal.model.AppImageLayout; | |
| import jdk.jpackage.internal.model.AppImageSigningConfig; | |
| import jdk.jpackage.internal.model.Application; | |
| import jdk.jpackage.internal.model.ApplicationLayout; | |
| import jdk.jpackage.internal.model.FileAssociation; | |
| import jdk.jpackage.internal.model.MacApplication; | |
| import jdk.jpackage.internal.model.MacFileAssociation; | |
| import jdk.jpackage.internal.model.MacPackage; | |
| import jdk.jpackage.internal.model.Package; | |
| import jdk.jpackage.internal.model.PackageType; | |
| import jdk.jpackage.internal.model.PackagerException; | |
| import jdk.jpackage.internal.util.PathUtils; | |
| import jdk.jpackage.internal.util.function.ThrowingConsumer; | |
| final class MacPackagingPipeline { | |
| enum MacBuildApplicationTaskID implements TaskID { | |
| RUNTIME_INFO_PLIST, | |
| COPY_JLILIB, | |
| APP_ICON, | |
| PKG_INFO_FILE, | |
| FA_ICONS, | |
| APP_INFO_PLIST, | |
| PACKAGE_FILE, | |
| SIGN | |
| } | |
| enum MacCopyAppImageTaskID implements TaskID { | |
| COPY_PACKAGE_FILE, | |
| COPY_RUNTIME_INFO_PLIST, | |
| REPLACE_APP_IMAGE_FILE, | |
| COPY_SIGN | |
| } | |
| static AppImageLayout packagingLayout(Package pkg) { | |
| return pkg.appImageLayout().resolveAt(pkg.relativeInstallDir().getFileName()); | |
| } | |
| static PackagingPipeline.Builder build(Optional<Package> pkg) { | |
| final var builder = PackagingPipeline.buildStandard() | |
| .appContextMapper(appContext -> { | |
| return new TaskContextProxy(appContext, true, false); | |
| }) | |
| .pkgContextMapper(appContext -> { | |
| final var isRuntimeInstaller = pkg.map(Package::isRuntimeInstaller).orElse(false); | |
| final var withPredefinedAppImage = pkg.flatMap(Package::predefinedAppImage).isPresent(); | |
| return new TaskContextProxy(appContext, false, isRuntimeInstaller || withPredefinedAppImage); | |
| }) | |
| .appImageLayoutForPackaging(MacPackagingPipeline::packagingLayout) | |
| .task(PackageTaskID.RUN_POST_IMAGE_USER_SCRIPT) | |
| .packageAction(MacPackagingPipeline::runPostAppImageUserScript).add() | |
| .task(CopyAppImageTaskID.COPY) | |
| .copyAction(MacPackagingPipeline::copyAppImage).add() | |
| .task(MacBuildApplicationTaskID.RUNTIME_INFO_PLIST) | |
| .applicationAction(MacPackagingPipeline::writeApplicationRuntimeInfoPlist) | |
| .addDependent(BuildApplicationTaskID.CONTENT).add() | |
| .task(MacBuildApplicationTaskID.COPY_JLILIB) | |
| .applicationAction(MacPackagingPipeline::copyJliLib) | |
| .addDependency(BuildApplicationTaskID.RUNTIME) | |
| .addDependent(BuildApplicationTaskID.CONTENT).add() | |
| .task(MacBuildApplicationTaskID.APP_ICON) | |
| .applicationAction(new ApplicationIcon()) | |
| .addDependent(BuildApplicationTaskID.CONTENT).add() | |
| .task(MacBuildApplicationTaskID.PKG_INFO_FILE) | |
| .applicationAction(MacPackagingPipeline::writePkgInfoFile) | |
| .addDependent(BuildApplicationTaskID.CONTENT).add() | |
| .task(MacBuildApplicationTaskID.PACKAGE_FILE) | |
| .packageAction(MacPackagingPipeline::writePackageFile) | |
| .addDependents(BuildApplicationTaskID.CONTENT).add() | |
| .task(MacCopyAppImageTaskID.REPLACE_APP_IMAGE_FILE) | |
| .addDependent(PrimaryTaskID.COPY_APP_IMAGE) | |
| .noaction().add() | |
| .task(MacCopyAppImageTaskID.COPY_PACKAGE_FILE) | |
| .packageAction(MacPackagingPipeline::writePackageFile) | |
| .addDependencies(CopyAppImageTaskID.COPY) | |
| .addDependents(PrimaryTaskID.COPY_APP_IMAGE).add() | |
| .task(MacCopyAppImageTaskID.COPY_RUNTIME_INFO_PLIST) | |
| .addDependencies(CopyAppImageTaskID.COPY) | |
| .addDependents(PrimaryTaskID.COPY_APP_IMAGE).add() | |
| .task(MacBuildApplicationTaskID.FA_ICONS) | |
| .applicationAction(MacPackagingPipeline::writeFileAssociationIcons) | |
| .addDependent(BuildApplicationTaskID.CONTENT).add() | |
| .task(MacBuildApplicationTaskID.APP_INFO_PLIST) | |
| .applicationAction(MacPackagingPipeline::writeAppInfoPlist) | |
| .addDependent(BuildApplicationTaskID.CONTENT).add(); | |
| builder.task(MacBuildApplicationTaskID.SIGN) | |
| .appImageAction(MacPackagingPipeline::sign) | |
| .addDependencies(builder.taskGraphSnapshot().getAllTailsOf(PrimaryTaskID.BUILD_APPLICATION_IMAGE)) | |
| .addDependent(PrimaryTaskID.BUILD_APPLICATION_IMAGE) | |
| .add(); | |
| builder.task(MacCopyAppImageTaskID.COPY_SIGN) | |
| .appImageAction(MacPackagingPipeline::sign) | |
| .addDependencies(builder.taskGraphSnapshot().getAllTailsOf(PrimaryTaskID.COPY_APP_IMAGE)) | |
| .addDependent(PrimaryTaskID.COPY_APP_IMAGE) | |
| .add(); | |
| pkg.ifPresent(p -> { | |
| final List<TaskID> disabledTasks = new ArrayList<>(); | |
| if (p.type() instanceof SignAppImagePackageType) { | |
| // This is a phony package signing predefined app image. | |
| // Don't create ".package" file. | |
| // Don't copy predefined app image, update it in place. | |
| // Disable running user script after app image ready. | |
| // Replace ".jpackage.xml" file. | |
| // Use app image layout. | |
| disabledTasks.add(MacCopyAppImageTaskID.COPY_PACKAGE_FILE); | |
| disabledTasks.add(CopyAppImageTaskID.COPY); | |
| disabledTasks.add(PackageTaskID.RUN_POST_IMAGE_USER_SCRIPT); | |
| builder.task(MacCopyAppImageTaskID.REPLACE_APP_IMAGE_FILE).applicationAction(createWriteAppImageFileAction()).add(); | |
| builder.appImageLayoutForPackaging(Package::appImageLayout); | |
| } else if (p.isRuntimeInstaller() || ((MacPackage)p).predefinedAppImageSigned().orElse(false)) { | |
| // If this is a runtime package or a signed predefined app image, | |
| // don't create ".package" file and don't sign it. | |
| disabledTasks.add(MacCopyAppImageTaskID.COPY_PACKAGE_FILE); | |
| disabledTasks.add(MacCopyAppImageTaskID.COPY_SIGN); | |
| // if (p.isRuntimeInstaller()) { | |
| // builder.task(MacCopyAppImageTaskID.COPY_RUNTIME_INFO_PLIST).packageAction(MacPackagingPipeline::writeRuntimeRuntimeInfoPlist).add(); | |
| // } | |
| } | |
| for (final var taskId : disabledTasks) { | |
| builder.task(taskId).noaction().add(); | |
| } | |
| }); | |
| return builder; | |
| } | |
| enum SignAppImagePackageType implements PackageType { | |
| VALUE; | |
| } | |
| static Package createSignAppImagePackage(MacApplication app, BuildEnv env) { | |
| if (!app.sign()) { | |
| throw new IllegalArgumentException(); | |
| } | |
| return toSupplier(() -> { | |
| return new PackageBuilder(app, SignAppImagePackageType.VALUE).predefinedAppImage( | |
| Objects.requireNonNull(env.appImageDir())).installDir(Path.of("/foo")).create(); | |
| }).get(); | |
| } | |
| private static void copyAppImage(MacPackage pkg, AppImageDesc srcAppImage, | |
| AppImageDesc dstAppImage) throws IOException { | |
| PackagingPipeline.copyAppImage(srcAppImage, dstAppImage, !pkg.predefinedAppImageSigned().orElse(false)); | |
| } | |
| private static void copyJliLib( | |
| AppImageBuildEnv<MacApplication, MacApplicationLayout> env) throws IOException { | |
| final var runtimeMacOSDir = env.resolvedLayout().runtimeRootDirectory().resolve("Contents/MacOS"); | |
| final var jliName = Path.of("libjli.dylib"); | |
| try (var walk = Files.walk(env.resolvedLayout().runtimeDirectory().resolve("lib"))) { | |
| final var jli = walk | |
| .filter(file -> file.getFileName().equals(jliName)) | |
| .findFirst() | |
| .orElseThrow(); | |
| Files.createDirectories(runtimeMacOSDir); | |
| Files.copy(jli, runtimeMacOSDir.resolve(jliName)); | |
| } | |
| } | |
| private static void runPostAppImageUserScript(PackageBuildEnv<Package, AppImageLayout> env) throws IOException { | |
| PackagingPipeline.runPostAppImageUserScript(new PackageBuildEnv<>( | |
| BuildEnv.withAppImageDir(env.env(), env.env().appImageDir().resolve(env.envLayout().rootDirectory())), | |
| env.pkg(), env.pkg().appImageLayout(), env.outputDir())); | |
| } | |
| private static void writePackageFile(PackageBuildEnv<Package, ApplicationLayout> env) throws IOException { | |
| new PackageFile(env.pkg().packageName()).save(env.resolvedLayout()); | |
| } | |
| private static void writePkgInfoFile( | |
| AppImageBuildEnv<MacApplication, MacApplicationLayout> env) throws IOException { | |
| final var dir = env.resolvedLayout().contentDirectory(); | |
| Files.createDirectories(dir); | |
| Files.write(dir.resolve("PkgInfo"), | |
| "APPL????".getBytes(StandardCharsets.ISO_8859_1)); | |
| } | |
| private static void writeRuntimeRuntimeInfoPlist(PackageBuildEnv<MacPackage, AppImageLayout> env) throws IOException { | |
| writeRuntimeInfoPlist(env.pkg().app(), env.env(), env.resolvedLayout().rootDirectory()); | |
| } | |
| private static void writeApplicationRuntimeInfoPlist( | |
| AppImageBuildEnv<MacApplication, MacApplicationLayout> env) throws IOException { | |
| writeRuntimeInfoPlist(env.app(), env.env(), env.resolvedLayout().runtimeRootDirectory()); | |
| } | |
| private static void writeRuntimeInfoPlist(MacApplication app, BuildEnv env, Path runtimeRootDirectory) throws IOException { | |
| Map<String, String> data = new HashMap<>(); | |
| data.put("CF_BUNDLE_IDENTIFIER", app.bundleIdentifier()); | |
| data.put("CF_BUNDLE_NAME", app.bundleName()); | |
| data.put("CF_BUNDLE_VERSION", app.version()); | |
| data.put("CF_BUNDLE_SHORT_VERSION_STRING", app.shortVersion().toString()); | |
| env.createResource("Runtime-Info.plist.template") | |
| .setPublicName("Runtime-Info.plist") | |
| .setCategory(I18N.getString("resource.runtime-info-plist")) | |
| .setSubstitutionData(data) | |
| .saveToFile(runtimeRootDirectory.resolve("Contents/Info.plist")); | |
| } | |
| private static void writeAppInfoPlist( | |
| AppImageBuildEnv<MacApplication, MacApplicationLayout> env) throws IOException { | |
| final var app = env.app(); | |
| final var infoPlistFile = env.resolvedLayout().contentDirectory().resolve("Info.plist"); | |
| Log.verbose(I18N.format("message.preparing-info-plist", PathUtils.normalizedAbsolutePathString(infoPlistFile))); | |
| final String faXml = toSupplier(() -> { | |
| var buf = new StringWriter(); | |
| var xml = XMLOutputFactory.newInstance().createXMLStreamWriter(buf); | |
| writeCFBundleDocumentTypes(xml, app); | |
| writeUTExportedTypeDeclarations(xml, app); | |
| xml.flush(); | |
| xml.close(); | |
| return buf.toString(); | |
| }).get(); | |
| Map<String, String> data = new HashMap<>(); | |
| data.put("DEPLOY_ICON_FILE", ApplicationIcon.getPath(app, env.resolvedLayout()).getFileName().toString()); | |
| data.put("DEPLOY_BUNDLE_COPYRIGHT", app.copyright()); | |
| data.put("DEPLOY_LAUNCHER_NAME", app.mainLauncher().orElseThrow().executableNameWithSuffix()); | |
| data.put("DEPLOY_BUNDLE_SHORT_VERSION", app.shortVersion().toString()); | |
| data.put("DEPLOY_BUNDLE_CFBUNDLE_VERSION", app.version()); | |
| data.put("DEPLOY_BUNDLE_NAME", app.bundleName()); | |
| data.put("DEPLOY_BUNDLE_IDENTIFIER", app.bundleIdentifier()); | |
| data.put("DEPLOY_APP_CATEGORY", app.category()); | |
| data.put("DEPLOY_FILE_ASSOCIATIONS", faXml); | |
| env.env().createResource("Info-lite.plist.template") | |
| .setCategory(I18N.getString("resource.app-info-plist")) | |
| .setSubstitutionData(data) | |
| .setPublicName("Info.plist") | |
| .saveToFile(infoPlistFile); | |
| } | |
| private static void sign(AppImageBuildEnv<MacApplication, MacApplicationLayout> env) throws IOException { | |
| final var app = env.app(); | |
| final var codesignConfigBuilder = CodesignConfig.build(); | |
| app.signingConfig().ifPresent(codesignConfigBuilder::from); | |
| if (app.sign() && app.signingConfig().flatMap(AppImageSigningConfig::entitlements).isEmpty()) { | |
| final var entitlementsDefaultResource = app.signingConfig().map( | |
| AppImageSigningConfig::entitlementsResourceName).orElseThrow(); | |
| final var entitlementsFile = env.env().configDir().resolve(app.name() + ".entitlements"); | |
| env.env().createResource(entitlementsDefaultResource) | |
| .setCategory(I18N.getString("resource.entitlements")) | |
| .saveToFile(entitlementsFile); | |
| codesignConfigBuilder.entitlements(entitlementsFile); | |
| } | |
| final Runnable signAction = () -> { | |
| final var appImageDir = env.resolvedLayout().rootDirectory(); | |
| AppImageSigner.createSigner(app, codesignConfigBuilder.create()).accept(appImageDir); | |
| }; | |
| app.signingConfig().flatMap(AppImageSigningConfig::keychain).map(Keychain::new).ifPresentOrElse(keychain -> { | |
| toBiConsumer(TempKeychain::withKeychain).accept(unused -> signAction.run(), keychain); | |
| }, signAction); | |
| } | |
| private static void writeCFBundleDocumentTypes(XMLStreamWriter xml, | |
| MacApplication app) throws XMLStreamException, IOException { | |
| final var fas = app.fileAssociations().toList(); | |
| if (!fas.isEmpty()) { | |
| writeKey(xml, "CFBundleDocumentTypes"); | |
| writeArray(xml, toXmlConsumer(() -> { | |
| for (var fa : fas) { | |
| writeDict(xml, toXmlConsumer(() -> { | |
| addFaToCFBundleDocumentTypes(xml, app, (MacFileAssociation) fa); | |
| })); | |
| } | |
| })); | |
| } | |
| } | |
| private static void writeUTExportedTypeDeclarations(XMLStreamWriter xml, | |
| MacApplication app) throws XMLStreamException, IOException { | |
| final var fas = app.fileAssociations().toList(); | |
| if (!fas.isEmpty()) { | |
| writeKey(xml, "UTExportedTypeDeclarations"); | |
| writeArray(xml, toXmlConsumer(() -> { | |
| for (var fa : fas) { | |
| writeDict(xml, toXmlConsumer(() -> { | |
| addFaToUTExportedTypeDeclarations(xml, app, (MacFileAssociation) fa); | |
| })); | |
| } | |
| })); | |
| } | |
| } | |
| private static String faContentType(MacApplication app, MacFileAssociation fa) { | |
| return String.format("%s.%s", app.bundleIdentifier(), fa.extension()); | |
| } | |
| private static void faWriteIcon(XMLStreamWriter xml, String key, FileAssociation fa) | |
| throws XMLStreamException { | |
| fa.icon().ifPresent(ThrowingConsumer.toConsumer(icon -> { | |
| writeString(xml, key, icon.getFileName()); | |
| })); | |
| } | |
| private static void addFaToCFBundleDocumentTypes(XMLStreamWriter xml, | |
| MacApplication app, MacFileAssociation fa) throws XMLStreamException, IOException { | |
| writeStringArray(xml, "LSItemContentTypes", List.of(faContentType(app, fa))); | |
| writeStringOptional(xml, "CFBundleTypeName", fa.description()); | |
| writeString(xml, "LSHandlerRank", fa.lsHandlerRank()); | |
| writeString(xml, "CFBundleTypeRole", fa.cfBundleTypeRole()); | |
| writeStringOptional(xml, "NSPersistentStoreTypeKey", fa.nsPersistentStoreTypeKey()); | |
| writeStringOptional(xml, "NSDocumentClass", fa.nsDocumentClass()); | |
| writeBoolean(xml, "LSIsAppleDefaultForType", true); | |
| writeBooleanOptional(xml, "LSTypeIsPackage", fa.lsTypeIsPackage()); | |
| writeBooleanOptional(xml, "LSSupportsOpeningDocumentsInPlace", fa.lsSupportsOpeningDocumentsInPlace()); | |
| writeBooleanOptional(xml, "UISupportsDocumentBrowser", fa.uiSupportsDocumentBrowser()); | |
| faWriteIcon(xml, "CFBundleTypeIconFile", fa); | |
| } | |
| private static void addFaToUTExportedTypeDeclarations(XMLStreamWriter xml, | |
| MacApplication app, MacFileAssociation fa) throws XMLStreamException, IOException { | |
| writeString(xml, "UTTypeIdentifier", faContentType(app, fa)); | |
| writeStringOptional(xml, "UTTypeDescription", fa.description()); | |
| writeStringArray(xml, "UTTypeConformsTo", fa.utTypeConformsTo()); | |
| faWriteIcon(xml, "UTTypeIconFile", fa); | |
| writeKey(xml, "UTTypeTagSpecification"); | |
| writeDict(xml, toXmlConsumer(() -> { | |
| writeStringArray(xml, "public.filename-extension", List.of(fa.extension())); | |
| writeStringArray(xml, "public.mime-type", List.of(fa.mimeType())); | |
| writeStringArray(xml, "NSExportableTypes", fa.nsExportableTypes()); | |
| })); | |
| } | |
| private static class ApplicationIcon implements ApplicationImageTaskAction<MacApplication, MacApplicationLayout> { | |
| static Path getPath(Application app, ApplicationLayout appLayout) { | |
| return appLayout.desktopIntegrationDirectory().resolve(app.name() + ".icns"); | |
| } | |
| @Override | |
| public void execute(AppImageBuildEnv<MacApplication, MacApplicationLayout> env) | |
| throws IOException { | |
| final var resource = env.env().createResource("JavaApp.icns").setCategory("icon"); | |
| env.app().icon().ifPresent(resource::setExternal); | |
| resource.saveToFile(getPath(env.app(), env.resolvedLayout())); | |
| } | |
| } | |
| private static void writeFileAssociationIcons(AppImageBuildEnv<MacApplication, MacApplicationLayout> env) throws IOException { | |
| for (var faIcon : env.app().fileAssociations() | |
| .filter(FileAssociation::hasIcon) | |
| .map(FileAssociation::icon) | |
| .map(Optional::get).toList()) { | |
| Files.copy(faIcon, env.resolvedLayout().desktopIntegrationDirectory().resolve(faIcon.getFileName())); | |
| } | |
| } | |
| private record TaskContextProxy(TaskContext delegate, boolean forApp, boolean copyAppImage) implements TaskContext { | |
| @Override | |
| public boolean test(TaskID taskID) { | |
| if (!delegate.test(taskID)) { | |
| return false; | |
| } else if (taskID == MacBuildApplicationTaskID.PACKAGE_FILE) { | |
| // Don't create files relevant for package bundling when bundling app image | |
| return !forApp; | |
| } else { | |
| return true; | |
| } | |
| } | |
| @Override | |
| public void execute(TaskAction taskAction) throws IOException, PackagerException { | |
| delegate.execute(taskAction); | |
| } | |
| } | |
| static final MacApplicationLayout APPLICATION_LAYOUT = MacApplicationLayout.create( | |
| ApplicationLayoutUtils.PLATFORM_APPLICATION_LAYOUT, Path.of("Contents/runtime")); | |
| } |