src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageBundler.java
branchJDK-8200758-branch
changeset 58647 2c43b89b1679
parent 58607 7451b17b40d3
child 58762 0fe62353385b
equal deleted inserted replaced
58608:a561014c28d0 58647:2c43b89b1679
    22  * or visit www.oracle.com if you need additional information or have any
    22  * or visit www.oracle.com if you need additional information or have any
    23  * questions.
    23  * questions.
    24  */
    24  */
    25 package jdk.jpackage.internal;
    25 package jdk.jpackage.internal;
    26 
    26 
    27 import java.awt.image.BufferedImage;
       
    28 import java.io.*;
    27 import java.io.*;
    29 import java.nio.file.InvalidPathException;
    28 import java.nio.file.InvalidPathException;
    30 import java.nio.file.Files;
       
    31 import java.nio.file.Path;
    29 import java.nio.file.Path;
    32 import java.text.MessageFormat;
    30 import java.text.MessageFormat;
    33 import java.util.*;
    31 import java.util.*;
    34 import java.util.function.Predicate;
    32 import java.util.function.Predicate;
    35 import java.util.stream.Collectors;
    33 import java.util.stream.Collectors;
    36 import java.util.stream.Stream;
    34 import java.util.stream.Stream;
    37 import javax.imageio.ImageIO;
    35 import static jdk.jpackage.internal.DesktopIntegration.*;
    38 import javax.xml.stream.XMLOutputFactory;
       
    39 import javax.xml.stream.XMLStreamException;
       
    40 import javax.xml.stream.XMLStreamWriter;
       
    41 import static jdk.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR;
    36 import static jdk.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR;
    42 import static jdk.jpackage.internal.LinuxAppBundler.LINUX_PACKAGE_DEPENDENCIES;
    37 import static jdk.jpackage.internal.LinuxAppBundler.LINUX_PACKAGE_DEPENDENCIES;
    43 import static jdk.jpackage.internal.LinuxAppImageBuilder.DEFAULT_ICON;
       
    44 import static jdk.jpackage.internal.LinuxAppImageBuilder.ICON_PNG;
       
    45 import static jdk.jpackage.internal.StandardBundlerParam.*;
    38 import static jdk.jpackage.internal.StandardBundlerParam.*;
    46 
    39 
    47 
    40 
    48 abstract class LinuxPackageBundler extends AbstractBundler {
    41 abstract class LinuxPackageBundler extends AbstractBundler {
    49 
       
    50     private static final String DESKTOP_COMMANDS_INSTALL = "DESKTOP_COMMANDS_INSTALL";
       
    51     private static final String DESKTOP_COMMANDS_UNINSTALL = "DESKTOP_COMMANDS_UNINSTALL";
       
    52     private static final String UTILITY_SCRIPTS = "UTILITY_SCRIPTS";
       
    53 
       
    54     private static final BundlerParamInfo<LinuxAppBundler> APP_BUNDLER =
       
    55         new StandardBundlerParam<>(
       
    56                 "linux.app.bundler",
       
    57                 LinuxAppBundler.class,
       
    58                 (params) -> new LinuxAppBundler(),
       
    59                 null
       
    60         );
       
    61 
       
    62     private static final BundlerParamInfo<String> MENU_GROUP =
       
    63         new StandardBundlerParam<>(
       
    64                 Arguments.CLIOptions.LINUX_MENU_GROUP.getId(),
       
    65                 String.class,
       
    66                 params -> I18N.getString("param.menu-group.default"),
       
    67                 (s, p) -> s
       
    68         );
       
    69 
       
    70     private static final StandardBundlerParam<Boolean> SHORTCUT_HINT =
       
    71         new StandardBundlerParam<>(
       
    72                 Arguments.CLIOptions.LINUX_SHORTCUT_HINT.getId(),
       
    73                 Boolean.class,
       
    74                 params -> false,
       
    75                 (s, p) -> (s == null || "null".equalsIgnoreCase(s))
       
    76                         ? false : Boolean.valueOf(s)
       
    77         );
       
    78 
    42 
    79     LinuxPackageBundler(BundlerParamInfo<String> packageName) {
    43     LinuxPackageBundler(BundlerParamInfo<String> packageName) {
    80         this.packageName = packageName;
    44         this.packageName = packageName;
    81     }
    45     }
    82 
       
    83     private final BundlerParamInfo<String> packageName;
       
    84     private boolean withFindNeededPackages;
       
    85 
    46 
    86     @Override
    47     @Override
    87     final public boolean validate(Map<String, ? super Object> params)
    48     final public boolean validate(Map<String, ? super Object> params)
    88             throws ConfigException {
    49             throws ConfigException {
    89 
    50 
   359                         I18N.getString(msgKey + ".advise"));
   320                         I18N.getString(msgKey + ".advise"));
   360             }
   321             }
   361         }
   322         }
   362     }
   323     }
   363 
   324 
   364     /**
   325     private final BundlerParamInfo<String> packageName;
   365      * Helper to create files for desktop integration.
   326     private boolean withFindNeededPackages;
   366      */
       
   367     private class DesktopIntegration {
       
   368 
       
   369         DesktopIntegration(PlatformPackage thePackage,
       
   370                 Map<String, ? super Object> params) {
       
   371 
       
   372             associations = FILE_ASSOCIATIONS.fetchFrom(params).stream().filter(
       
   373                     a -> {
       
   374                         if (a == null) {
       
   375                             return false;
       
   376                         }
       
   377                         List<String> mimes = FA_CONTENT_TYPE.fetchFrom(a);
       
   378                         return (mimes != null && !mimes.isEmpty());
       
   379                     }).collect(Collectors.toUnmodifiableList());
       
   380 
       
   381             launchers = ADD_LAUNCHERS.fetchFrom(params);
       
   382 
       
   383             this.thePackage = thePackage;
       
   384 
       
   385             customIconFile = ICON_PNG.fetchFrom(params);
       
   386 
       
   387             verbose = VERBOSE.fetchFrom(params);
       
   388             resourceDir = RESOURCE_DIR.fetchFrom(params);
       
   389 
       
   390             // XDG recommends to use vendor prefix in desktop file names as xdg
       
   391             // commands copy files to system directories.
       
   392             // Package name should be a good prefix.
       
   393             final String desktopFileName = String.format("%s-%s.desktop",
       
   394                         thePackage.name(), APP_NAME.fetchFrom(params));
       
   395             final String mimeInfoFileName = String.format("%s-%s-MimeInfo.xml",
       
   396                         thePackage.name(), APP_NAME.fetchFrom(params));
       
   397 
       
   398             mimeInfoFile = new DesktopFile(mimeInfoFileName);
       
   399 
       
   400             if (!associations.isEmpty() || SHORTCUT_HINT.fetchFrom(params) || customIconFile != null) {
       
   401                 //
       
   402                 // Create primary .desktop file if one of conditions is met:
       
   403                 // - there are file associations configured
       
   404                 // - user explicitely requested to create a shortcut
       
   405                 // - custom icon specified
       
   406                 //
       
   407                 desktopFile = new DesktopFile(desktopFileName);
       
   408                 iconFile = new DesktopFile(String.format("%s.png",
       
   409                         APP_NAME.fetchFrom(params)));
       
   410             } else {
       
   411                 desktopFile = null;
       
   412                 iconFile = null;
       
   413             }
       
   414 
       
   415             desktopFileData = Collections.unmodifiableMap(
       
   416                     createDataForDesktopFile(params));
       
   417 
       
   418             nestedIntegrations = launchers.stream().map(
       
   419                     launcherParams -> new DesktopIntegration(thePackage,
       
   420                             launcherParams)).collect(Collectors.toList());
       
   421         }
       
   422 
       
   423         List<String> requiredPackages() {
       
   424             return Stream.of(List.of(this), nestedIntegrations).flatMap(
       
   425                     List::stream).map(DesktopIntegration::requiredPackagesSelf).flatMap(
       
   426                     List::stream).distinct().collect(Collectors.toList());
       
   427         }
       
   428 
       
   429         Map<String, String> create() throws IOException {
       
   430             if (iconFile != null) {
       
   431                 // Create application icon file.
       
   432                 prepareSrcIconFile();
       
   433             }
       
   434 
       
   435             Map<String, String> data = new HashMap<>(desktopFileData);
       
   436 
       
   437             final ShellCommands shellCommands;
       
   438             if (desktopFile != null) {
       
   439                 // Create application desktop description file.
       
   440                 createDesktopFile(data);
       
   441 
       
   442                 // Shell commands will be created only if desktop file
       
   443                 // should be installed.
       
   444                 shellCommands = new ShellCommands();
       
   445             } else {
       
   446                 shellCommands = null;
       
   447             }
       
   448 
       
   449             if (!associations.isEmpty()) {
       
   450                 // Create XML file with mime types corresponding to file associations.
       
   451                 createFileAssociationsMimeInfoFile();
       
   452 
       
   453                 shellCommands.setFileAssociations();
       
   454 
       
   455                 // Create icon files corresponding to file associations
       
   456                 Map<String, Path> mimeTypeWithIconFile = createFileAssociationIconFiles();
       
   457                 mimeTypeWithIconFile.forEach((k, v) -> {
       
   458                     shellCommands.addIcon(k, v);
       
   459                 });
       
   460             }
       
   461 
       
   462             // Create shell commands to install/uninstall integration with desktop of the app.
       
   463             if (shellCommands != null) {
       
   464                 shellCommands.applyTo(data);
       
   465             }
       
   466 
       
   467             boolean needCleanupScripts = !associations.isEmpty();
       
   468 
       
   469             // Take care of additional launchers if there are any.
       
   470             // Process every additional launcher as the main application launcher.
       
   471             // Collect shell commands to install/uninstall integration with desktop
       
   472             // of the additional launchers and append them to the corresponding
       
   473             // commands of the main launcher.
       
   474             List<String> installShellCmds = new ArrayList<>(Arrays.asList(
       
   475                     data.get(DESKTOP_COMMANDS_INSTALL)));
       
   476             List<String> uninstallShellCmds = new ArrayList<>(Arrays.asList(
       
   477                     data.get(DESKTOP_COMMANDS_UNINSTALL)));
       
   478             for (var integration: nestedIntegrations) {
       
   479                 if (!integration.associations.isEmpty()) {
       
   480                     needCleanupScripts = true;
       
   481                 }
       
   482 
       
   483                 Map<String, String> launcherData = integration.create();
       
   484 
       
   485                 installShellCmds.add(launcherData.get(DESKTOP_COMMANDS_INSTALL));
       
   486                 uninstallShellCmds.add(launcherData.get(
       
   487                         DESKTOP_COMMANDS_UNINSTALL));
       
   488             }
       
   489 
       
   490             data.put(DESKTOP_COMMANDS_INSTALL, stringifyShellCommands(
       
   491                     installShellCmds));
       
   492             data.put(DESKTOP_COMMANDS_UNINSTALL, stringifyShellCommands(
       
   493                     uninstallShellCmds));
       
   494 
       
   495             if (needCleanupScripts) {
       
   496                 // Pull in utils.sh scrips library.
       
   497                 try (InputStream is = getResourceAsStream("utils.sh");
       
   498                         InputStreamReader isr = new InputStreamReader(is);
       
   499                         BufferedReader reader = new BufferedReader(isr)) {
       
   500                     data.put(UTILITY_SCRIPTS, reader.lines().collect(
       
   501                             Collectors.joining(System.lineSeparator())));
       
   502                 }
       
   503             } else {
       
   504                 data.put(UTILITY_SCRIPTS, "");
       
   505             }
       
   506 
       
   507             return data;
       
   508         }
       
   509 
       
   510         private List<String> requiredPackagesSelf() {
       
   511             if (desktopFile != null) {
       
   512                 return List.of("xdg-utils");
       
   513             }
       
   514             return Collections.emptyList();
       
   515         }
       
   516 
       
   517         private Map<String, String> createDataForDesktopFile(
       
   518                 Map<String, ? super Object> params) {
       
   519             Map<String, String> data = new HashMap<>();
       
   520             data.put("APPLICATION_NAME", APP_NAME.fetchFrom(params));
       
   521             data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params));
       
   522             data.put("APPLICATION_ICON",
       
   523                     iconFile != null ? iconFile.installPath().toString() : null);
       
   524             data.put("DEPLOY_BUNDLE_CATEGORY", MENU_GROUP.fetchFrom(params));
       
   525             data.put("APPLICATION_LAUNCHER",
       
   526                     thePackage.installedApplicationLayout().launchersDirectory().resolve(
       
   527                             LinuxAppImageBuilder.getLauncherName(params)).toString());
       
   528 
       
   529             return data;
       
   530         }
       
   531 
       
   532         /**
       
   533          * Shell commands to integrate something with desktop.
       
   534          */
       
   535         private class ShellCommands {
       
   536 
       
   537             ShellCommands() {
       
   538                 registerIconCmds = new ArrayList<>();
       
   539                 unregisterIconCmds = new ArrayList<>();
       
   540 
       
   541                 registerDesktopFileCmd = String.join(" ", "xdg-desktop-menu",
       
   542                         "install", desktopFile.installPath().toString());
       
   543                 unregisterDesktopFileCmd = String.join(" ", "xdg-desktop-menu",
       
   544                         "uninstall", desktopFile.installPath().toString());
       
   545             }
       
   546 
       
   547             void setFileAssociations() {
       
   548                 registerFileAssociationsCmd = String.join(" ", "xdg-mime",
       
   549                         "install",
       
   550                         mimeInfoFile.installPath().toString());
       
   551                 unregisterFileAssociationsCmd = String.join(" ", "xdg-mime",
       
   552                         "uninstall", mimeInfoFile.installPath().toString());
       
   553 
       
   554                 //
       
   555                 // Add manual cleanup of system files to get rid of
       
   556                 // the default mime type handlers.
       
   557                 //
       
   558                 // Even after mime type is unregisterd with `xdg-mime uninstall`
       
   559                 // command and desktop file deleted with `xdg-desktop-menu uninstall`
       
   560                 // command, records in
       
   561                 // `/usr/share/applications/defaults.list` (Ubuntu 16) or
       
   562                 // `/usr/local/share/applications/defaults.list` (OracleLinux 7)
       
   563                 // files remain referencing deleted mime time and deleted
       
   564                 // desktop file which makes `xdg-mime query default` output name
       
   565                 // of non-existing desktop file.
       
   566                 //
       
   567                 String cleanUpCommand = String.join(" ",
       
   568                         "uninstall_default_mime_handler",
       
   569                         desktopFile.installPath().getFileName().toString(),
       
   570                         String.join(" ", getMimeTypeNamesFromFileAssociations()));
       
   571 
       
   572                 unregisterFileAssociationsCmd = stringifyShellCommands(
       
   573                         unregisterFileAssociationsCmd, cleanUpCommand);
       
   574             }
       
   575 
       
   576             void addIcon(String mimeType, Path iconFile) {
       
   577                 final int imgSize = getSquareSizeOfImage(iconFile.toFile());
       
   578                 final String dashMime = mimeType.replace('/', '-');
       
   579                 registerIconCmds.add(String.join(" ", "xdg-icon-resource",
       
   580                         "install", "--context", "mimetypes", "--size ",
       
   581                         Integer.toString(imgSize), iconFile.toString(), dashMime));
       
   582                 unregisterIconCmds.add(String.join(" ", "xdg-icon-resource",
       
   583                         "uninstall", dashMime));
       
   584             }
       
   585 
       
   586             void applyTo(Map<String, String> data) {
       
   587                 List<String> cmds = new ArrayList<>();
       
   588 
       
   589                 cmds.add(registerDesktopFileCmd);
       
   590                 cmds.add(registerFileAssociationsCmd);
       
   591                 cmds.addAll(registerIconCmds);
       
   592                 data.put(DESKTOP_COMMANDS_INSTALL, stringifyShellCommands(cmds));
       
   593 
       
   594                 cmds.clear();
       
   595                 cmds.add(unregisterDesktopFileCmd);
       
   596                 cmds.add(unregisterFileAssociationsCmd);
       
   597                 cmds.addAll(unregisterIconCmds);
       
   598                 data.put(DESKTOP_COMMANDS_UNINSTALL, stringifyShellCommands(cmds));
       
   599             }
       
   600 
       
   601             private String registerDesktopFileCmd;
       
   602             private String unregisterDesktopFileCmd;
       
   603 
       
   604             private String registerFileAssociationsCmd;
       
   605             private String unregisterFileAssociationsCmd;
       
   606 
       
   607             private List<String> registerIconCmds;
       
   608             private List<String> unregisterIconCmds;
       
   609         }
       
   610 
       
   611         private final PlatformPackage thePackage;
       
   612 
       
   613         private final List<Map<String, ? super Object>> associations;
       
   614 
       
   615         private final List<Map<String, ? super Object>> launchers;
       
   616 
       
   617         /**
       
   618          * Desktop integration file. xml, icon, etc.
       
   619          * Resides somewhere in application installation tree.
       
   620          * Has two paths:
       
   621          *  - path where it should be placed at package build time;
       
   622          *  - path where it should be installed by package manager;
       
   623          */
       
   624         private class DesktopFile {
       
   625 
       
   626             DesktopFile(String fileName) {
       
   627                 installPath = thePackage
       
   628                         .installedApplicationLayout()
       
   629                         .destktopIntegrationDirectory().resolve(fileName);
       
   630                 srcPath = thePackage
       
   631                         .sourceApplicationLayout()
       
   632                         .destktopIntegrationDirectory().resolve(fileName);
       
   633             }
       
   634 
       
   635             private final Path installPath;
       
   636             private final Path srcPath;
       
   637 
       
   638             Path installPath() {
       
   639                 return installPath;
       
   640             }
       
   641 
       
   642             Path srcPath() {
       
   643                 return srcPath;
       
   644             }
       
   645         }
       
   646 
       
   647         private final boolean verbose;
       
   648         private final File resourceDir;
       
   649 
       
   650         private final DesktopFile mimeInfoFile;
       
   651         private final DesktopFile desktopFile;
       
   652         private final DesktopFile iconFile;
       
   653 
       
   654         final private List<DesktopIntegration> nestedIntegrations;
       
   655 
       
   656         private final Map<String, String> desktopFileData;
       
   657 
       
   658         /**
       
   659          * Path to icon file provided by user or null.
       
   660          */
       
   661         private final File customIconFile;
       
   662 
       
   663         private void appendFileAssociation(XMLStreamWriter xml,
       
   664                 Map<String, ? super Object> assoc) throws XMLStreamException {
       
   665 
       
   666             xml.writeStartElement("mime-type");
       
   667             final String thisMime = FA_CONTENT_TYPE.fetchFrom(assoc).get(0);
       
   668             xml.writeAttribute("type", thisMime);
       
   669 
       
   670             final String description = FA_DESCRIPTION.fetchFrom(assoc);
       
   671             if (description != null && !description.isEmpty()) {
       
   672                 xml.writeStartElement("comment");
       
   673                 xml.writeCharacters(description);
       
   674                 xml.writeEndElement();
       
   675             }
       
   676 
       
   677             final List<String> extensions = FA_EXTENSIONS.fetchFrom(assoc);
       
   678             if (extensions == null) {
       
   679                 Log.error(I18N.getString(
       
   680                         "message.creating-association-with-null-extension"));
       
   681             } else {
       
   682                 for (String ext : extensions) {
       
   683                     xml.writeStartElement("glob");
       
   684                     xml.writeAttribute("pattern", "*." + ext);
       
   685                     xml.writeEndElement();
       
   686                 }
       
   687             }
       
   688 
       
   689             xml.writeEndElement();
       
   690         }
       
   691 
       
   692         private void createFileAssociationsMimeInfoFile() throws IOException {
       
   693             XMLOutputFactory xmlFactory = XMLOutputFactory.newInstance();
       
   694 
       
   695             try (Writer w = new BufferedWriter(new FileWriter(
       
   696                     mimeInfoFile.srcPath().toFile()))) {
       
   697                 XMLStreamWriter xml = xmlFactory.createXMLStreamWriter(w);
       
   698 
       
   699                 xml.writeStartDocument();
       
   700                 xml.writeStartElement("mime-info");
       
   701                 xml.writeNamespace("xmlns",
       
   702                       "https://www.freedesktop.org/standards/shared-mime-info");
       
   703 
       
   704                 for (var assoc : associations) {
       
   705                     appendFileAssociation(xml, assoc);
       
   706                 }
       
   707 
       
   708                 xml.writeEndElement();
       
   709                 xml.writeEndDocument();
       
   710                 xml.flush();
       
   711                 xml.close();
       
   712 
       
   713             } catch (XMLStreamException ex) {
       
   714                 Log.verbose(ex);
       
   715                 throw new IOException(ex);
       
   716             }
       
   717         }
       
   718 
       
   719         private Map<String, Path> createFileAssociationIconFiles() throws
       
   720                 IOException {
       
   721             Map<String, Path> mimeTypeWithIconFile = new HashMap<>();
       
   722             for (var assoc : associations) {
       
   723                 File customFaIcon = FA_ICON.fetchFrom(assoc);
       
   724                 if (customFaIcon == null || !customFaIcon.exists() || getSquareSizeOfImage(
       
   725                         customFaIcon) == 0) {
       
   726                     continue;
       
   727                 }
       
   728 
       
   729                 String fname = iconFile.srcPath().getFileName().toString();
       
   730                 if (fname.indexOf(".") > 0) {
       
   731                     fname = fname.substring(0, fname.lastIndexOf("."));
       
   732                 }
       
   733 
       
   734                 DesktopFile faIconFile = new DesktopFile(
       
   735                         fname + "_fa_" + customFaIcon.getName());
       
   736 
       
   737                 IOUtils.copyFile(customFaIcon, faIconFile.srcPath().toFile());
       
   738 
       
   739                 mimeTypeWithIconFile.put(FA_CONTENT_TYPE.fetchFrom(assoc).get(0),
       
   740                         faIconFile.installPath());
       
   741             }
       
   742             return mimeTypeWithIconFile;
       
   743         }
       
   744 
       
   745         private void createDesktopFile(Map<String, String> data) throws IOException {
       
   746             List<String> mimeTypes = getMimeTypeNamesFromFileAssociations();
       
   747             data.put("DESKTOP_MIMES", "MimeType=" + String.join(";", mimeTypes));
       
   748 
       
   749             // prepare desktop shortcut
       
   750             try (Writer w = Files.newBufferedWriter(desktopFile.srcPath())) {
       
   751                 String content = preprocessTextResource(
       
   752                         desktopFile.srcPath().getFileName().toString(),
       
   753                         I18N.getString("resource.menu-shortcut-descriptor"),
       
   754                         "template.desktop",
       
   755                         data,
       
   756                         verbose,
       
   757                         resourceDir);
       
   758                 w.write(content);
       
   759             }
       
   760         }
       
   761 
       
   762         private void prepareSrcIconFile() throws IOException {
       
   763             if (customIconFile == null || !customIconFile.exists()) {
       
   764                 fetchResource(iconFile.srcPath().getFileName().toString(),
       
   765                         I18N.getString("resource.menu-icon"),
       
   766                         DEFAULT_ICON,
       
   767                         iconFile.srcPath().toFile(),
       
   768                         verbose,
       
   769                         resourceDir);
       
   770             } else {
       
   771                 fetchResource(iconFile.srcPath().getFileName().toString(),
       
   772                         I18N.getString("resource.menu-icon"),
       
   773                         customIconFile,
       
   774                         iconFile.srcPath().toFile(),
       
   775                         verbose,
       
   776                         resourceDir);
       
   777             }
       
   778         }
       
   779 
       
   780         private List<String> getMimeTypeNamesFromFileAssociations() {
       
   781             return associations.stream().map(
       
   782                     a -> FA_CONTENT_TYPE.fetchFrom(a).get(0)).collect(
       
   783                             Collectors.toUnmodifiableList());
       
   784         }
       
   785     }
       
   786 
       
   787     private static int getSquareSizeOfImage(File f) {
       
   788         try {
       
   789             BufferedImage bi = ImageIO.read(f);
       
   790             if (bi.getWidth() == bi.getHeight()) {
       
   791                 return bi.getWidth();
       
   792             }
       
   793         } catch (IOException e) {
       
   794             Log.verbose(e);
       
   795         }
       
   796         return 0;
       
   797     }
       
   798 
       
   799     private static String stringifyShellCommands(String ... commands) {
       
   800         return stringifyShellCommands(Arrays.asList(commands));
       
   801     }
       
   802 
       
   803     private static String stringifyShellCommands(List<String> commands) {
       
   804         return String.join(System.lineSeparator(), commands.stream().filter(
       
   805                 s -> s != null && !s.isEmpty()).collect(Collectors.toList()));
       
   806     }
       
   807 
       
   808     private DesktopIntegration desktopIntegration;
   327     private DesktopIntegration desktopIntegration;
       
   328 
       
   329     private static final BundlerParamInfo<LinuxAppBundler> APP_BUNDLER =
       
   330         new StandardBundlerParam<>(
       
   331                 "linux.app.bundler",
       
   332                 LinuxAppBundler.class,
       
   333                 (params) -> new LinuxAppBundler(),
       
   334                 null
       
   335         );
       
   336 
   809 }
   337 }