/
githubmirror
/
panama-vector
Обзор
Документация
Войти
/
githubmirror
/
panama-vector
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java
508 строк
20 KB
Alexey Semenyuk
8374839: Improve jpackage information messages
05 май 2026, 00:00
05 май 2026, 00:00
06e6539
Код
Авторство
О чём код?
/* * Copyright (c) 2019, 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.ApplicationImageUtils.createLauncherIconResource; import static jdk.jpackage.internal.model.LauncherShortcut.toRequest; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Stream; import javax.imageio.ImageIO; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; import jdk.jpackage.internal.model.FileAssociation; import jdk.jpackage.internal.model.LauncherShortcut; import jdk.jpackage.internal.model.LinuxLauncher; import jdk.jpackage.internal.model.LinuxPackage; import jdk.jpackage.internal.model.Package; import jdk.jpackage.internal.util.CompositeProxy; import jdk.jpackage.internal.util.Enquoter; import jdk.jpackage.internal.util.PathUtils; import jdk.jpackage.internal.util.XmlUtils; /** * Helper to create files for desktop integration. */ final class DesktopIntegration extends ShellCustomAction { private static final String COMMANDS_INSTALL = "DESKTOP_COMMANDS_INSTALL"; private static final String COMMANDS_UNINSTALL = "DESKTOP_COMMANDS_UNINSTALL"; private static final String SCRIPTS = "DESKTOP_SCRIPTS"; private static final String COMMON_SCRIPTS = "COMMON_SCRIPTS"; private static final List<String> REPLACEMENT_STRING_IDS = List.of( COMMANDS_INSTALL, COMMANDS_UNINSTALL, SCRIPTS, COMMON_SCRIPTS); private DesktopIntegration(BuildEnv env, LinuxPackage pkg, LinuxLauncher launcher) { associations = launcher.fileAssociations().stream().map( LinuxFileAssociation::create).toList(); this.env = env; this.pkg = pkg; this.launcher = launcher; // Need desktop and icon files if one of conditions is met: // - there are file associations configured // - user explicitly requested to create a shortcut boolean withDesktopFile = !associations.isEmpty() || toRequest(launcher.shortcut()).orElse(false); if (!launcher.hasIcon()) { // This is additional launcher with explicit `no icon` configuration. withDesktopFile = false; } else if (launcher.hasCustomIcon()) { // This launcher has custom icon configured. withDesktopFile = true; } desktopFileResource = env.createResource("template.desktop") .setCategory(I18N.getString("resource.menu-shortcut-descriptor")) .setPublicName(launcher.name() + ".desktop"); final String escapedAppFileName = launcher.name().replaceAll("\\s+", "_"); // XDG recommends to use vendor prefix in desktop file names as xdg // commands copy files to system directories. // Package name should be a good prefix. final String desktopFileName = String.format("%s-%s.desktop", pkg.packageName(), escapedAppFileName); final String mimeInfoFileName = String.format("%s-%s-MimeInfo.xml", pkg.packageName(), escapedAppFileName); mimeInfoFile = createDesktopFile(mimeInfoFileName); if (withDesktopFile) { desktopFile = Optional.of(createDesktopFile(desktopFileName)); iconFile = Optional.of(createDesktopFile(escapedAppFileName + ".png")); } else { desktopFile = Optional.empty(); iconFile = Optional.empty(); } iconResource = createLauncherIconResource(launcher, env::createResource); desktopFileData = createDataForDesktopFile(); if (launcher != pkg.app().mainLauncher().orElseThrow()) { nestedIntegrations = List.of(); } else { nestedIntegrations = pkg.app().additionalLaunchers().stream().map(v -> { return (LinuxLauncher)v; }).filter(l -> { return toRequest(l.shortcut()).orElse(true); }).map(l -> { return new DesktopIntegration(env, pkg, l); }).toList(); } } static ShellCustomAction create(BuildEnv env, Package pkg) { if (pkg.isRuntimeInstaller()) { return ShellCustomAction.nop(REPLACEMENT_STRING_IDS); } return new DesktopIntegration(env, (LinuxPackage) pkg, (LinuxLauncher) pkg.app().mainLauncher().orElseThrow()); } @Override List<String> requiredPackages() { return Stream.of(List.of(this), nestedIntegrations).flatMap( List::stream).map(DesktopIntegration::requiredPackagesSelf).flatMap( List::stream).distinct().toList(); } @Override protected List<String> replacementStringIds() { return REPLACEMENT_STRING_IDS; } @Override protected Map<String, String> createImpl() throws IOException { if (iconFile.isPresent()) { // Create application icon file. iconResource.orElseThrow().saveToFile(iconFile.orElseThrow().srcPath()); } Map<String, String> data = new HashMap<>(desktopFileData); final ShellCommands shellCommands; if (desktopFile.isPresent()) { // Create application desktop description file. saveDesktopFile(data); // Shell commands will be created only if desktop file // should be installed. shellCommands = new ShellCommands(); } else { shellCommands = null; } if (!associations.isEmpty()) { // Create XML file with mime types corresponding to file associations. createFileAssociationsMimeInfoFile(); shellCommands.setFileAssociations(); // Create icon files corresponding to file associations addFileAssociationIconFiles(shellCommands); } // Create shell commands to install/uninstall integration with desktop of the app. if (shellCommands != null) { shellCommands.applyTo(data); } // Take care of additional launchers if there are any. // Process every additional launcher as the main application launcher. // Collect shell commands to install/uninstall integration with desktop // of the additional launchers and append them to the corresponding // commands of the main launcher. List<String> installShellCmds = new ArrayList<>(Arrays.asList( data.get(COMMANDS_INSTALL))); List<String> uninstallShellCmds = new ArrayList<>(Arrays.asList( data.get(COMMANDS_UNINSTALL))); for (var integration: nestedIntegrations) { Map<String, String> launcherData = integration.create(); installShellCmds.add(launcherData.get(COMMANDS_INSTALL)); uninstallShellCmds.add(launcherData.get(COMMANDS_UNINSTALL)); } data.put(COMMANDS_INSTALL, stringifyShellCommands(installShellCmds)); data.put(COMMANDS_UNINSTALL, stringifyShellCommands(uninstallShellCmds)); data.put(COMMON_SCRIPTS, stringifyTextFile("common_utils.sh")); data.put(SCRIPTS, stringifyTextFile("desktop_utils.sh")); return data; } private List<String> requiredPackagesSelf() { return desktopFile.map(file -> List.of("xdg-utils")).orElseGet(Collections::emptyList); } private Map<String, String> createDataForDesktopFile() { var installedLayout = pkg.asInstalledPackageApplicationLayout().orElseThrow(); Map<String, String> data = new HashMap<>(); data.put("APPLICATION_NAME", launcher.name()); data.put("APPLICATION_DESCRIPTION", launcher.description()); data.put("APPLICATION_ICON", iconFile.map( f -> f.installPath().toString()).orElse(null)); data.put("DEPLOY_BUNDLE_CATEGORY", pkg.menuGroupName()); data.put("APPLICATION_LAUNCHER", Enquoter.forPropertyValues().applyTo( installedLayout.launchersDirectory().resolve(launcher.executableNameWithSuffix()).toString())); data.put("STARTUP_DIRECTORY", launcher.shortcut() .flatMap(LauncherShortcut::startupDirectory) .map(startupDirectory -> { switch (startupDirectory) { case DEFAULT -> { return (Path)null; } case APP_DIR -> { return installedLayout.appDirectory(); } default -> { throw new AssertionError(); } } }).map(str -> { return "Path=" + str; }).orElse(null)); return data; } /** * Shell commands to integrate something with desktop. */ private class ShellCommands { ShellCommands() { registerIconCmds = new ArrayList<>(); unregisterIconCmds = new ArrayList<>(); final var desktopFileInstallPath = desktopFile.orElseThrow().installPath().toString(); registerDesktopFileCmd = String.join(" ", "xdg-desktop-menu", "install", desktopFileInstallPath); unregisterDesktopFileCmd = String.join(" ", "do_if_file_belongs_to_single_package", desktopFileInstallPath, "xdg-desktop-menu", "uninstall", desktopFileInstallPath); } void setFileAssociations() { registerFileAssociationsCmd = String.join(" ", "xdg-mime", "install", mimeInfoFile.installPath().toString()); unregisterFileAssociationsCmd = String.join(" ", "do_if_file_belongs_to_single_package", mimeInfoFile. installPath().toString(), "xdg-mime", "uninstall", mimeInfoFile.installPath().toString()); final var desktopFileInstallPath = desktopFile.orElseThrow().installPath(); // // Add manual cleanup of system files to get rid of // the default mime type handlers. // // Even after mime type is unregistered with `xdg-mime uninstall` // command and desktop file deleted with `xdg-desktop-menu uninstall` // command, records in // `/usr/share/applications/defaults.list` (Ubuntu 16) or // `/usr/local/share/applications/defaults.list` (OracleLinux 7) // files remain referencing deleted mime time and deleted // desktop file which makes `xdg-mime query default` output name // of non-existing desktop file. // String cleanUpCommand = String.join(" ", "do_if_file_belongs_to_single_package", desktopFileInstallPath.toString(), "desktop_uninstall_default_mime_handler", desktopFileInstallPath.getFileName().toString(), String.join(" ", getMimeTypeNamesFromFileAssociations())); unregisterFileAssociationsCmd = stringifyShellCommands( unregisterFileAssociationsCmd, cleanUpCommand); } void addIcon(String mimeType, Path iconFile, int imgSize) { imgSize = normalizeIconSize(imgSize); final String dashMime = mimeType.replace('/', '-'); registerIconCmds.add(String.join(" ", "xdg-icon-resource", "install", "--context", "mimetypes", "--size", Integer.toString(imgSize), iconFile.toString(), dashMime)); unregisterIconCmds.add(String.join(" ", "do_if_file_belongs_to_single_package", iconFile.toString(), "xdg-icon-resource", "uninstall", dashMime, "--size", Integer.toString(imgSize))); } void applyTo(Map<String, String> data) { List<String> cmds = new ArrayList<>(); cmds.add(registerDesktopFileCmd); cmds.add(registerFileAssociationsCmd); cmds.addAll(registerIconCmds); data.put(COMMANDS_INSTALL, stringifyShellCommands(cmds)); cmds.clear(); cmds.add(unregisterDesktopFileCmd); cmds.add(unregisterFileAssociationsCmd); cmds.addAll(unregisterIconCmds); data.put(COMMANDS_UNINSTALL, stringifyShellCommands(cmds)); } private String registerDesktopFileCmd; private String unregisterDesktopFileCmd; private String registerFileAssociationsCmd; private String unregisterFileAssociationsCmd; private List<String> registerIconCmds; private List<String> unregisterIconCmds; } /** * Creates desktop integration file. xml, icon, etc. * * Returned instance: * - srcPath(): path where it should be placed at package build time; * - installPath(): path where it should be installed by package manager; */ private InstallableFile createDesktopFile(String fileName) { var srcPath = env.asApplicationLayout().orElseThrow().desktopIntegrationDirectory().resolve(fileName); var installPath = pkg.asInstalledPackageApplicationLayout().orElseThrow().desktopIntegrationDirectory().resolve(fileName); return new InstallableFile(srcPath, installPath); } private void appendFileAssociation(XMLStreamWriter xml, LinuxFileAssociation fa) throws XMLStreamException { xml.writeStartElement("mime-type"); xml.writeAttribute("type", fa.mimeType()); final var description = fa.description().orElse(null); if (description != null) { xml.writeStartElement("comment"); xml.writeCharacters(description); xml.writeEndElement(); } xml.writeStartElement("glob"); xml.writeAttribute("pattern", "*." + fa.extension()); xml.writeEndElement(); xml.writeEndElement(); } private void createFileAssociationsMimeInfoFile() throws IOException { XmlUtils.createXml(mimeInfoFile.srcPath(), xml -> { xml.writeStartElement("mime-info"); xml.writeDefaultNamespace( "http://www.freedesktop.org/standards/shared-mime-info"); for (var fa : associations) { appendFileAssociation(xml, fa); } xml.writeEndElement(); }); } private void addFileAssociationIconFiles(ShellCommands shellCommands) throws IOException { Set<String> processedMimeTypes = new HashSet<>(); for (var fa : associations) { if (!fa.hasIcon()) { // No icon. continue; } var mimeType = fa.mimeType(); if (processedMimeTypes.contains(mimeType)) { continue; } processedMimeTypes.add(mimeType); final var faIcon = fa.icon().orElseThrow(); // Create icon name for mime type from mime type. var faIconFile = createDesktopFile(mimeType.replace(File.separatorChar, '-') + PathUtils.getSuffix(faIcon)); IOUtils.copyFile(faIcon, faIconFile.srcPath()); shellCommands.addIcon(mimeType, faIconFile.installPath(), fa.iconSize()); } } private void saveDesktopFile(Map<String, String> data) throws IOException { List<String> mimeTypes = getMimeTypeNamesFromFileAssociations(); data.put("DESKTOP_MIMES", "MimeType=" + String.join(";", mimeTypes)); // prepare desktop shortcut desktopFileResource .setSubstitutionData(data) .saveToFile(desktopFile.orElseThrow().srcPath()); } private List<String> getMimeTypeNamesFromFileAssociations() { return associations.stream().map(FileAssociation::mimeType).toList(); } private static int getSquareSizeOfImage(Path path) { try { BufferedImage bi = ImageIO.read(path.toFile()); return Math.max(bi.getWidth(), bi.getHeight()); } catch (IOException e) { Log.trace(e, "Failed to get dimensions of an image at [%s]", path); } return 0; } private static int normalizeIconSize(int iconSize) { // If register icon with "uncommon" size, it will be ignored. // So find the best matching "common" size. List<Integer> commonIconSizes = List.of(16, 22, 32, 48, 64, 128); int idx = Collections.binarySearch(commonIconSizes, iconSize); if (idx < 0) { // Given icon size is greater than the largest common icon size. return commonIconSizes.get(commonIconSizes.size() - 1); } if (idx == 0) { // Given icon size is less or equal than the smallest common icon size. return commonIconSizes.get(idx); } int commonIconSize = commonIconSizes.get(idx); if (iconSize < commonIconSize) { // It is better to scale down original icon than to scale it up for // better visual quality. commonIconSize = commonIconSizes.get(idx - 1); } return commonIconSize; } private interface LinuxFileAssociationMixin { int iconSize(); record Stub(int iconSize) implements LinuxFileAssociationMixin {} } private static interface LinuxFileAssociation extends FileAssociation, LinuxFileAssociationMixin { static LinuxFileAssociation create(FileAssociation fa) { var iconSize = getIconSize(fa); if (iconSize <= 0) { // nullify the icon fa = new FileAssociation.Stub(fa.description(), Optional.empty(), fa.mimeType(), fa.extension()); } return CompositeProxy.build() .invokeTunnel(CompositeProxyTunnel.INSTANCE) .create(LinuxFileAssociation.class, fa, new LinuxFileAssociationMixin.Stub(iconSize)); } private static int getIconSize(FileAssociation fa) { return Optional.of(fa) .flatMap(FileAssociation::icon) .map(DesktopIntegration::getSquareSizeOfImage) .orElse(-1); } } private final BuildEnv env; private final LinuxPackage pkg; private final LinuxLauncher launcher; private final List<LinuxFileAssociation> associations; private final Optional<OverridableResource> iconResource; private final OverridableResource desktopFileResource; private final InstallableFile mimeInfoFile; private final Optional<InstallableFile> desktopFile; private final Optional<InstallableFile> iconFile; private final List<DesktopIntegration> nestedIntegrations; private final Map<String, String> desktopFileData; }