1 /*
   2  * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package jdk.jpackage.internal;
  27 
  28 import javax.imageio.ImageIO;
  29 import java.awt.image.BufferedImage;
  30 import java.io.*;
  31 import java.nio.charset.StandardCharsets;
  32 import java.nio.file.FileVisitResult;
  33 import java.nio.file.Files;
  34 import java.nio.file.Path;
  35 import java.nio.file.SimpleFileVisitor;
  36 import java.nio.file.StandardCopyOption;
  37 import java.nio.file.attribute.BasicFileAttributes;
  38 
  39 import java.nio.file.attribute.PosixFilePermission;
  40 import java.nio.file.attribute.PosixFilePermissions;
  41 import java.text.MessageFormat;
  42 import java.util.*;
  43 import java.util.regex.Pattern;
  44 import java.util.stream.Stream;
  45 
  46 import static jdk.jpackage.internal.StandardBundlerParam.*;
  47 import static jdk.jpackage.internal.LinuxAppBundler.ICON_PNG;
  48 import static jdk.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR;
  49 import static jdk.jpackage.internal.LinuxAppBundler.LINUX_PACKAGE_DEPENDENCIES;
  50 
  51 public class LinuxDebBundler extends AbstractBundler {
  52 
  53     private static final ResourceBundle I18N = ResourceBundle.getBundle(
  54                     "jdk.jpackage.internal.resources.LinuxResources");
  55 
  56     public static final BundlerParamInfo<LinuxAppBundler> APP_BUNDLER =
  57             new StandardBundlerParam<>(
  58             "linux.app.bundler",
  59             LinuxAppBundler.class,
  60             params -> new LinuxAppBundler(),
  61             (s, p) -> null);
  62 
  63     // Debian rules for package naming are used here
  64     // https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Source
  65     //
  66     // Package names must consist only of lower case letters (a-z),
  67     // digits (0-9), plus (+) and minus (-) signs, and periods (.).
  68     // They must be at least two characters long and
  69     // must start with an alphanumeric character.
  70     //
  71     private static final Pattern DEB_BUNDLE_NAME_PATTERN =
  72             Pattern.compile("^[a-z][a-z\\d\\+\\-\\.]+");
  73 
  74     public static final BundlerParamInfo<String> BUNDLE_NAME =
  75             new StandardBundlerParam<> (
  76             Arguments.CLIOptions.LINUX_BUNDLE_NAME.getId(),
  77             String.class,
  78             params -> {
  79                 String nm = APP_NAME.fetchFrom(params);
  80 
  81                 if (nm == null) return null;
  82 
  83                 // make sure to lower case and spaces/underscores become dashes
  84                 nm = nm.toLowerCase().replaceAll("[ _]", "-");
  85                 return nm;
  86             },
  87             (s, p) -> {
  88                 if (!DEB_BUNDLE_NAME_PATTERN.matcher(s).matches()) {
  89                     throw new IllegalArgumentException(new ConfigException(
  90                             MessageFormat.format(I18N.getString(
  91                             "error.invalid-value-for-package-name"), s),
  92                             I18N.getString(
  93                             "error.invalid-value-for-package-name.advice")));
  94                 }
  95 
  96                 return s;
  97             });
  98 
  99     public static final BundlerParamInfo<String> FULL_PACKAGE_NAME =
 100             new StandardBundlerParam<> (
 101             "linux.deb.fullPackageName",
 102             String.class,
 103             params -> BUNDLE_NAME.fetchFrom(params) + "-"
 104                     + VERSION.fetchFrom(params),
 105             (s, p) -> s);
 106 
 107     public static final BundlerParamInfo<File> DEB_IMAGE_DIR =
 108             new StandardBundlerParam<>(
 109             "linux.deb.imageDir",
 110             File.class,
 111             params -> {
 112                 File imagesRoot = IMAGES_ROOT.fetchFrom(params);
 113                 if (!imagesRoot.exists()) imagesRoot.mkdirs();
 114                 return new File(new File(imagesRoot, "linux-deb.image"),
 115                         FULL_PACKAGE_NAME.fetchFrom(params));
 116             },
 117             (s, p) -> new File(s));
 118 
 119     public static final BundlerParamInfo<File> APP_IMAGE_ROOT =
 120             new StandardBundlerParam<>(
 121             "linux.deb.imageRoot",
 122             File.class,
 123             params -> {
 124                 File imageDir = DEB_IMAGE_DIR.fetchFrom(params);
 125                 return new File(imageDir, LINUX_INSTALL_DIR.fetchFrom(params));
 126             },
 127             (s, p) -> new File(s));
 128 
 129     public static final BundlerParamInfo<File> CONFIG_DIR =
 130             new StandardBundlerParam<>(
 131             "linux.deb.configDir",
 132             File.class,
 133             params ->  new File(DEB_IMAGE_DIR.fetchFrom(params), "DEBIAN"),
 134             (s, p) -> new File(s));
 135 
 136     public static final BundlerParamInfo<String> EMAIL =
 137             new StandardBundlerParam<> (
 138             Arguments.CLIOptions.LINUX_DEB_MAINTAINER.getId(),
 139             String.class,
 140             params -> "Unknown",
 141             (s, p) -> s);
 142 
 143     public static final BundlerParamInfo<String> MAINTAINER =
 144             new StandardBundlerParam<> (
 145             BundleParams.PARAM_MAINTAINER,
 146             String.class,
 147             params -> VENDOR.fetchFrom(params) + " <"
 148                     + EMAIL.fetchFrom(params) + ">",
 149             (s, p) -> s);
 150 
 151     public static final BundlerParamInfo<String> SECTION =
 152             new StandardBundlerParam<>(
 153             Arguments.CLIOptions.LINUX_CATEGORY.getId(),
 154             String.class,
 155             params -> "misc",
 156             (s, p) -> s);
 157 
 158     public static final BundlerParamInfo<String> LICENSE_TEXT =
 159             new StandardBundlerParam<> (
 160             "linux.deb.licenseText",
 161             String.class,
 162             params -> {
 163                 try {
 164                     String licenseFile = LICENSE_FILE.fetchFrom(params);
 165                     if (licenseFile != null) {
 166                         StringBuilder contentBuilder = new StringBuilder();
 167                         try (Stream<String> stream = Files.lines(Path.of(
 168                                 licenseFile), StandardCharsets.UTF_8)) {
 169                             stream.forEach(s -> contentBuilder.append(s).append(
 170                                     "\n"));
 171                         }
 172                         return contentBuilder.toString();
 173                     }
 174                 } catch (Exception e) {
 175                     Log.verbose(e);
 176                 }
 177                 return "Unknown";
 178             },
 179             (s, p) -> s);
 180 
 181     public static final BundlerParamInfo<String> COPYRIGHT_FILE =
 182             new StandardBundlerParam<>(
 183             Arguments.CLIOptions.LINUX_DEB_COPYRIGHT_FILE.getId(),
 184             String.class,
 185             params -> null,
 186             (s, p) -> s);
 187 
 188     public static final BundlerParamInfo<String> XDG_FILE_PREFIX =
 189             new StandardBundlerParam<> (
 190             "linux.xdg-prefix",
 191             String.class,
 192             params -> {
 193                 try {
 194                     String vendor;
 195                     if (params.containsKey(VENDOR.getID())) {
 196                         vendor = VENDOR.fetchFrom(params);
 197                     } else {
 198                         vendor = "jpackage";
 199                     }
 200                     String appName = APP_NAME.fetchFrom(params);
 201 
 202                     return (appName + "-" + vendor).replaceAll("\\s", "");
 203                 } catch (Exception e) {
 204                     Log.verbose(e);
 205                 }
 206                 return "unknown-MimeInfo.xml";
 207             },
 208             (s, p) -> s);
 209 
 210     public static final BundlerParamInfo<String> MENU_GROUP =
 211         new StandardBundlerParam<>(
 212                 Arguments.CLIOptions.LINUX_MENU_GROUP.getId(),
 213                 String.class,
 214                 params -> I18N.getString("param.menu-group.default"),
 215                 (s, p) -> s
 216         );
 217 
 218     private final static String DEFAULT_ICON = "javalogo_white_32.png";
 219     private final static String DEFAULT_CONTROL_TEMPLATE = "template.control";
 220     private final static String DEFAULT_PRERM_TEMPLATE = "template.prerm";
 221     private final static String DEFAULT_PREINSTALL_TEMPLATE =
 222             "template.preinst";
 223     private final static String DEFAULT_POSTRM_TEMPLATE = "template.postrm";
 224     private final static String DEFAULT_POSTINSTALL_TEMPLATE =
 225             "template.postinst";
 226     private final static String DEFAULT_COPYRIGHT_TEMPLATE =
 227             "template.copyright";
 228     private final static String DEFAULT_DESKTOP_FILE_TEMPLATE =
 229             "template.desktop";
 230 
 231     private final static String TOOL_DPKG_DEB = "dpkg-deb";
 232     private final static String TOOL_DPKG = "dpkg";
 233 
 234     public static boolean testTool(String toolName, String minVersion) {
 235         try {
 236             ProcessBuilder pb = new ProcessBuilder(
 237                     toolName,
 238                     "--version");
 239             // not interested in the output
 240             IOUtils.exec(pb, true, null);
 241         } catch (Exception e) {
 242             Log.verbose(MessageFormat.format(I18N.getString(
 243                     "message.test-for-tool"), toolName, e.getMessage()));
 244             return false;
 245         }
 246         return true;
 247     }
 248 
 249     @Override
 250     public boolean validate(Map<String, ? super Object> params)
 251             throws ConfigException {
 252         try {
 253             if (params == null) throw new ConfigException(
 254                     I18N.getString("error.parameters-null"),
 255                     I18N.getString("error.parameters-null.advice"));
 256 
 257             //run basic validation to ensure requirements are met
 258             //we are not interested in return code, only possible exception
 259             APP_BUNDLER.fetchFrom(params).validate(params);
 260 
 261             // NOTE: Can we validate that the required tools are available
 262             // before we start?
 263             if (!testTool(TOOL_DPKG_DEB, "1")){
 264                 throw new ConfigException(MessageFormat.format(
 265                         I18N.getString("error.tool-not-found"), TOOL_DPKG_DEB),
 266                         I18N.getString("error.tool-not-found.advice"));
 267             }
 268             if (!testTool(TOOL_DPKG, "1")){
 269                 throw new ConfigException(MessageFormat.format(
 270                         I18N.getString("error.tool-not-found"), TOOL_DPKG),
 271                         I18N.getString("error.tool-not-found.advice"));
 272             }
 273 
 274 
 275             // Show warning is license file is missing
 276             String licenseFile = LICENSE_FILE.fetchFrom(params);
 277             if (licenseFile == null) {
 278                 Log.verbose(I18N.getString("message.debs-like-licenses"));
 279             }
 280 
 281             // only one mime type per association, at least one file extention
 282             List<Map<String, ? super Object>> associations =
 283                     FILE_ASSOCIATIONS.fetchFrom(params);
 284             if (associations != null) {
 285                 for (int i = 0; i < associations.size(); i++) {
 286                     Map<String, ? super Object> assoc = associations.get(i);
 287                     List<String> mimes = FA_CONTENT_TYPE.fetchFrom(assoc);
 288                     if (mimes == null || mimes.isEmpty()) {
 289                         String msgKey =
 290                             "error.no-content-types-for-file-association";
 291                         throw new ConfigException(
 292                                 MessageFormat.format(I18N.getString(msgKey), i),
 293                                 I18N.getString(msgKey + ".advise"));
 294 
 295                     } else if (mimes.size() > 1) {
 296                         String msgKey =
 297                             "error.too-many-content-types-for-file-association";
 298                         throw new ConfigException(
 299                                 MessageFormat.format(I18N.getString(msgKey), i),
 300                                 I18N.getString(msgKey + ".advise"));
 301                     }
 302                 }
 303             }
 304 
 305             // bundle name has some restrictions
 306             // the string converter will throw an exception if invalid
 307             BUNDLE_NAME.getStringConverter().apply(
 308                     BUNDLE_NAME.fetchFrom(params), params);
 309 
 310             return true;
 311         } catch (RuntimeException re) {
 312             if (re.getCause() instanceof ConfigException) {
 313                 throw (ConfigException) re.getCause();
 314             } else {
 315                 throw new ConfigException(re);
 316             }
 317         }
 318     }
 319 
 320     private boolean prepareProto(Map<String, ? super Object> params)
 321             throws PackagerException, IOException {
 322         File appImage = StandardBundlerParam.getPredefinedAppImage(params);
 323 
 324         // we either have an application image or need to build one
 325         if (appImage != null) {
 326             // copy everything from appImage dir into appDir/name
 327             IOUtils.copyRecursive(appImage.toPath(),
 328                     getConfig_RootDirectory(params).toPath());
 329         } else {
 330             File bundleDir = APP_BUNDLER.fetchFrom(params).doBundle(params,
 331                     APP_IMAGE_ROOT.fetchFrom(params), true);
 332             if (bundleDir == null) {
 333                 return false;
 334             }
 335             Files.move(bundleDir.toPath(), getConfig_RootDirectory(
 336                     params).toPath(), StandardCopyOption.REPLACE_EXISTING);
 337         }
 338         return true;
 339     }
 340 
 341     public File bundle(Map<String, ? super Object> params,
 342             File outdir) throws PackagerException {
 343 
 344         IOUtils.writableOutputDir(outdir.toPath());
 345 
 346         // we want to create following structure
 347         //   <package-name>
 348         //        DEBIAN
 349         //          control   (file with main package details)
 350         //          menu      (request to create menu)
 351         //          ... other control files if needed ....
 352         //        opt  (by default)
 353         //          AppFolder (this is where app image goes)
 354         //             launcher executable
 355         //             app
 356         //             runtime
 357 
 358         File imageDir = DEB_IMAGE_DIR.fetchFrom(params);
 359         File configDir = CONFIG_DIR.fetchFrom(params);
 360 
 361         try {
 362 
 363             imageDir.mkdirs();
 364             configDir.mkdirs();
 365             if (prepareProto(params) && prepareProjectConfig(params)) {
 366                 adjustPermissionsRecursive(imageDir);
 367                 return buildDeb(params, outdir);
 368             }
 369             return null;
 370         } catch (IOException ex) {
 371             Log.verbose(ex);
 372             throw new PackagerException(ex);
 373         }
 374     }
 375 
 376     /*
 377      * set permissions with a string like "rwxr-xr-x"
 378      *
 379      * This cannot be directly backport to 22u which is built with 1.6
 380      */
 381     private void setPermissions(File file, String permissions) {
 382         Set<PosixFilePermission> filePermissions =
 383                 PosixFilePermissions.fromString(permissions);
 384         try {
 385             if (file.exists()) {
 386                 Files.setPosixFilePermissions(file.toPath(), filePermissions);
 387             }
 388         } catch (IOException ex) {
 389             Log.error(ex.getMessage());
 390             Log.verbose(ex);
 391         }
 392 
 393     }
 394 
 395     private static String getDebArch() throws IOException {
 396         try (var baos = new ByteArrayOutputStream();
 397                 var ps = new PrintStream(baos)) {
 398             var pb = new ProcessBuilder(TOOL_DPKG, "--print-architecture");
 399             IOUtils.exec(pb, false, ps);
 400             return baos.toString().split("\n", 2)[0];
 401         }
 402     }
 403 
 404     private long getInstalledSizeKB(Map<String, ? super Object> params) {
 405         return getInstalledSizeKB(APP_IMAGE_ROOT.fetchFrom(params)) >> 10;
 406     }
 407 
 408     private long getInstalledSizeKB(File dir) {
 409         long count = 0;
 410         File[] children = dir.listFiles();
 411         if (children != null) {
 412             for (File file : children) {
 413                 if (file.isFile()) {
 414                     count += file.length();
 415                 }
 416                 else if (file.isDirectory()) {
 417                     count += getInstalledSizeKB(file);
 418                 }
 419             }
 420         }
 421         return count;
 422     }
 423 
 424     private void adjustPermissionsRecursive(File dir) throws IOException {
 425         Files.walkFileTree(dir.toPath(), new SimpleFileVisitor<Path>() {
 426             @Override
 427             public FileVisitResult visitFile(Path file,
 428                     BasicFileAttributes attrs)
 429                     throws IOException {
 430                 if (file.endsWith(".so") || !Files.isExecutable(file)) {
 431                     setPermissions(file.toFile(), "rw-r--r--");
 432                 } else if (Files.isExecutable(file)) {
 433                     setPermissions(file.toFile(), "rwxr-xr-x");
 434                 }
 435                 return FileVisitResult.CONTINUE;
 436             }
 437 
 438             @Override
 439             public FileVisitResult postVisitDirectory(Path dir, IOException e)
 440                     throws IOException {
 441                 if (e == null) {
 442                     setPermissions(dir.toFile(), "rwxr-xr-x");
 443                     return FileVisitResult.CONTINUE;
 444                 } else {
 445                     // directory iteration failed
 446                     throw e;
 447                 }
 448             }
 449         });
 450     }
 451 
 452     private boolean prepareProjectConfig(Map<String, ? super Object> params)
 453             throws IOException {
 454         Map<String, String> data = createReplacementData(params);
 455         File rootDir = getConfig_RootDirectory(params);
 456         File binDir = new File(rootDir, "bin");
 457 
 458         File iconTarget = getConfig_IconFile(binDir, params);
 459         File icon = ICON_PNG.fetchFrom(params);
 460         if (!StandardBundlerParam.isRuntimeInstaller(params)) {
 461             // prepare installer icon
 462             if (icon == null || !icon.exists()) {
 463                 fetchResource(iconTarget.getName(),
 464                         I18N.getString("resource.menu-icon"),
 465                         DEFAULT_ICON,
 466                         iconTarget,
 467                         VERBOSE.fetchFrom(params),
 468                         RESOURCE_DIR.fetchFrom(params));
 469             } else {
 470                 fetchResource(iconTarget.getName(),
 471                         I18N.getString("resource.menu-icon"),
 472                         icon,
 473                         iconTarget,
 474                         VERBOSE.fetchFrom(params),
 475                         RESOURCE_DIR.fetchFrom(params));
 476             }
 477         }
 478 
 479         StringBuilder installScripts = new StringBuilder();
 480         StringBuilder removeScripts = new StringBuilder();
 481         for (Map<String, ? super Object> addLauncher :
 482                 ADD_LAUNCHERS.fetchFrom(params)) {
 483             Map<String, String> addLauncherData =
 484                     createReplacementData(addLauncher);
 485             addLauncherData.put("APPLICATION_FS_NAME",
 486                     data.get("APPLICATION_FS_NAME"));
 487             addLauncherData.put("DESKTOP_MIMES", "");
 488 
 489             if (!StandardBundlerParam.isRuntimeInstaller(params)) {
 490                 // prepare desktop shortcut
 491                 try (Writer w = Files.newBufferedWriter(
 492                         getConfig_DesktopShortcutFile(
 493                                 binDir, addLauncher).toPath())) {
 494                     String content = preprocessTextResource(
 495                             getConfig_DesktopShortcutFile(binDir,
 496                             addLauncher).getName(),
 497                             I18N.getString("resource.menu-shortcut-descriptor"),
 498                             DEFAULT_DESKTOP_FILE_TEMPLATE,
 499                             addLauncherData,
 500                             VERBOSE.fetchFrom(params),
 501                             RESOURCE_DIR.fetchFrom(params));
 502                     w.write(content);
 503                 }
 504             }
 505 
 506             // prepare installer icon
 507             iconTarget = getConfig_IconFile(binDir, addLauncher);
 508             icon = ICON_PNG.fetchFrom(addLauncher);
 509             if (icon == null || !icon.exists()) {
 510                 fetchResource(iconTarget.getName(),
 511                         I18N.getString("resource.menu-icon"),
 512                         DEFAULT_ICON,
 513                         iconTarget,
 514                         VERBOSE.fetchFrom(params),
 515                         RESOURCE_DIR.fetchFrom(params));
 516             } else {
 517                 fetchResource(iconTarget.getName(),
 518                         I18N.getString("resource.menu-icon"),
 519                         icon,
 520                         iconTarget,
 521                         VERBOSE.fetchFrom(params),
 522                         RESOURCE_DIR.fetchFrom(params));
 523             }
 524 
 525             // postinst copying of desktop icon
 526             installScripts.append(
 527                     "        xdg-desktop-menu install --novendor ");
 528             installScripts.append(LINUX_INSTALL_DIR.fetchFrom(params));
 529             installScripts.append("/");
 530             installScripts.append(data.get("APPLICATION_FS_NAME"));
 531             installScripts.append("/bin/");
 532             installScripts.append(
 533                     addLauncherData.get("APPLICATION_LAUNCHER_FILENAME"));
 534             installScripts.append(".desktop\n");
 535 
 536             // postrm cleanup of desktop icon
 537             removeScripts.append(
 538                     "        xdg-desktop-menu uninstall --novendor ");
 539             removeScripts.append(LINUX_INSTALL_DIR.fetchFrom(params));
 540             removeScripts.append("/");
 541             removeScripts.append(data.get("APPLICATION_FS_NAME"));
 542             removeScripts.append("/bin/");
 543             removeScripts.append(
 544                     addLauncherData.get("APPLICATION_LAUNCHER_FILENAME"));
 545             removeScripts.append(".desktop\n");
 546         }
 547         data.put("ADD_LAUNCHERS_INSTALL", installScripts.toString());
 548         data.put("ADD_LAUNCHERS_REMOVE", removeScripts.toString());
 549 
 550         List<Map<String, ? super Object>> associations =
 551                 FILE_ASSOCIATIONS.fetchFrom(params);
 552         data.put("FILE_ASSOCIATION_INSTALL", "");
 553         data.put("FILE_ASSOCIATION_REMOVE", "");
 554         data.put("DESKTOP_MIMES", "");
 555         if (associations != null) {
 556             String mimeInfoFile = XDG_FILE_PREFIX.fetchFrom(params)
 557                     + "-MimeInfo.xml";
 558             StringBuilder mimeInfo = new StringBuilder(
 559                 "<?xml version=\"1.0\"?>\n<mime-info xmlns="
 560                 + "'http://www.freedesktop.org/standards/shared-mime-info'>\n");
 561             StringBuilder registrations = new StringBuilder();
 562             StringBuilder deregistrations = new StringBuilder();
 563             StringBuilder desktopMimes = new StringBuilder("MimeType=");
 564             boolean addedEntry = false;
 565 
 566             for (Map<String, ? super Object> assoc : associations) {
 567                 //  <mime-type type="application/x-vnd.awesome">
 568                 //    <comment>Awesome document</comment>
 569                 //    <glob pattern="*.awesome"/>
 570                 //    <glob pattern="*.awe"/>
 571                 //  </mime-type>
 572 
 573                 if (assoc == null) {
 574                     continue;
 575                 }
 576 
 577                 String description = FA_DESCRIPTION.fetchFrom(assoc);
 578                 File faIcon = FA_ICON.fetchFrom(assoc);
 579                 List<String> extensions = FA_EXTENSIONS.fetchFrom(assoc);
 580                 if (extensions == null) {
 581                     Log.error(I18N.getString(
 582                           "message.creating-association-with-null-extension"));
 583                 }
 584 
 585                 List<String> mimes = FA_CONTENT_TYPE.fetchFrom(assoc);
 586                 if (mimes == null || mimes.isEmpty()) {
 587                     continue;
 588                 }
 589                 String thisMime = mimes.get(0);
 590                 String dashMime = thisMime.replace('/', '-');
 591 
 592                 mimeInfo.append("  <mime-type type='")
 593                         .append(thisMime)
 594                         .append("'>\n");
 595                 if (description != null && !description.isEmpty()) {
 596                     mimeInfo.append("    <comment>")
 597                             .append(description)
 598                             .append("</comment>\n");
 599                 }
 600 
 601                 if (extensions != null) {
 602                     for (String ext : extensions) {
 603                         mimeInfo.append("    <glob pattern='*.")
 604                                 .append(ext)
 605                                 .append("'/>\n");
 606                     }
 607                 }
 608 
 609                 mimeInfo.append("  </mime-type>\n");
 610                 if (!addedEntry) {
 611                     registrations.append("        xdg-mime install ")
 612                             .append(LINUX_INSTALL_DIR.fetchFrom(params))
 613                             .append("/")
 614                             .append(data.get("APPLICATION_FS_NAME"))
 615                             .append("/bin/")
 616                             .append(mimeInfoFile)
 617                             .append("\n");
 618 
 619                     deregistrations.append("        xdg-mime uninstall ")
 620                             .append(LINUX_INSTALL_DIR.fetchFrom(params))
 621                             .append("/")
 622                             .append(data.get("APPLICATION_FS_NAME"))
 623                             .append("/bin/")
 624                             .append(mimeInfoFile)
 625                             .append("\n");
 626                     addedEntry = true;
 627                 } else {
 628                     desktopMimes.append(";");
 629                 }
 630                 desktopMimes.append(thisMime);
 631 
 632                 if (faIcon != null && faIcon.exists()) {
 633                     int size = getSquareSizeOfImage(faIcon);
 634 
 635                     if (size > 0) {
 636                         File target = new File(binDir,
 637                                 APP_NAME.fetchFrom(params)
 638                                 + "_fa_" + faIcon.getName());
 639                         IOUtils.copyFile(faIcon, target);
 640 
 641                         // xdg-icon-resource install --context mimetypes
 642                         // --size 64 awesomeapp_fa_1.png
 643                         // application-x.vnd-awesome
 644                         registrations.append(
 645                                 "        xdg-icon-resource install "
 646                                         + "--context mimetypes --size ")
 647                                 .append(size)
 648                                 .append(" ")
 649                                 .append(LINUX_INSTALL_DIR.fetchFrom(params))
 650                                 .append("/")
 651                                 .append(data.get("APPLICATION_FS_NAME"))
 652                                 .append("/")
 653                                 .append(target.getName())
 654                                 .append(" ")
 655                                 .append(dashMime)
 656                                 .append("\n");
 657 
 658                         // x dg-icon-resource uninstall --context mimetypes
 659                         // --size 64 awesomeapp_fa_1.png
 660                         // application-x.vnd-awesome
 661                         deregistrations.append(
 662                                 "        xdg-icon-resource uninstall "
 663                                         + "--context mimetypes --size ")
 664                                 .append(size)
 665                                 .append(" ")
 666                                 .append(LINUX_INSTALL_DIR.fetchFrom(params))
 667                                 .append("/")
 668                                 .append(data.get("APPLICATION_FS_NAME"))
 669                                 .append("/")
 670                                 .append(target.getName())
 671                                 .append(" ")
 672                                 .append(dashMime)
 673                                 .append("\n");
 674                     }
 675                 }
 676             }
 677             mimeInfo.append("</mime-info>");
 678 
 679             if (addedEntry) {
 680                 try (Writer w = Files.newBufferedWriter(
 681                         new File(binDir, mimeInfoFile).toPath())) {
 682                     w.write(mimeInfo.toString());
 683                 }
 684                 data.put("FILE_ASSOCIATION_INSTALL", registrations.toString());
 685                 data.put("FILE_ASSOCIATION_REMOVE", deregistrations.toString());
 686                 data.put("DESKTOP_MIMES", desktopMimes.toString());
 687             }
 688         }
 689 
 690         if (!StandardBundlerParam.isRuntimeInstaller(params)) {
 691             //prepare desktop shortcut
 692             try (Writer w = Files.newBufferedWriter(
 693                     getConfig_DesktopShortcutFile(binDir, params).toPath())) {
 694                 String content = preprocessTextResource(
 695                         getConfig_DesktopShortcutFile(
 696                         binDir, params).getName(),
 697                         I18N.getString("resource.menu-shortcut-descriptor"),
 698                         DEFAULT_DESKTOP_FILE_TEMPLATE,
 699                         data,
 700                         VERBOSE.fetchFrom(params),
 701                         RESOURCE_DIR.fetchFrom(params));
 702                 w.write(content);
 703             }
 704         }
 705         // prepare control file
 706         try (Writer w = Files.newBufferedWriter(
 707                 getConfig_ControlFile(params).toPath())) {
 708             String content = preprocessTextResource(
 709                     getConfig_ControlFile(params).getName(),
 710                     I18N.getString("resource.deb-control-file"),
 711                     DEFAULT_CONTROL_TEMPLATE,
 712                     data,
 713                     VERBOSE.fetchFrom(params),
 714                     RESOURCE_DIR.fetchFrom(params));
 715             w.write(content);
 716         }
 717 
 718         try (Writer w = Files.newBufferedWriter(
 719                 getConfig_PreinstallFile(params).toPath())) {
 720             String content = preprocessTextResource(
 721                     getConfig_PreinstallFile(params).getName(),
 722                     I18N.getString("resource.deb-preinstall-script"),
 723                     DEFAULT_PREINSTALL_TEMPLATE,
 724                     data,
 725                     VERBOSE.fetchFrom(params),
 726                     RESOURCE_DIR.fetchFrom(params));
 727             w.write(content);
 728         }
 729         setPermissions(getConfig_PreinstallFile(params), "rwxr-xr-x");
 730 
 731         try (Writer w = Files.newBufferedWriter(
 732                     getConfig_PrermFile(params).toPath())) {
 733             String content = preprocessTextResource(
 734                     getConfig_PrermFile(params).getName(),
 735                     I18N.getString("resource.deb-prerm-script"),
 736                     DEFAULT_PRERM_TEMPLATE,
 737                     data,
 738                     VERBOSE.fetchFrom(params),
 739                     RESOURCE_DIR.fetchFrom(params));
 740             w.write(content);
 741         }
 742         setPermissions(getConfig_PrermFile(params), "rwxr-xr-x");
 743 
 744         try (Writer w = Files.newBufferedWriter(
 745                 getConfig_PostinstallFile(params).toPath())) {
 746             String content = preprocessTextResource(
 747                     getConfig_PostinstallFile(params).getName(),
 748                     I18N.getString("resource.deb-postinstall-script"),
 749                     DEFAULT_POSTINSTALL_TEMPLATE,
 750                     data,
 751                     VERBOSE.fetchFrom(params),
 752                     RESOURCE_DIR.fetchFrom(params));
 753             w.write(content);
 754         }
 755         setPermissions(getConfig_PostinstallFile(params), "rwxr-xr-x");
 756 
 757         try (Writer w = Files.newBufferedWriter(
 758                 getConfig_PostrmFile(params).toPath())) {
 759             String content = preprocessTextResource(
 760                     getConfig_PostrmFile(params).getName(),
 761                     I18N.getString("resource.deb-postrm-script"),
 762                     DEFAULT_POSTRM_TEMPLATE,
 763                     data,
 764                     VERBOSE.fetchFrom(params),
 765                     RESOURCE_DIR.fetchFrom(params));
 766             w.write(content);
 767         }
 768         setPermissions(getConfig_PostrmFile(params), "rwxr-xr-x");
 769 
 770         getConfig_CopyrightFile(params).getParentFile().mkdirs();
 771         String customCopyrightFile = COPYRIGHT_FILE.fetchFrom(params);
 772         if (customCopyrightFile != null) {
 773             IOUtils.copyFile(new File(customCopyrightFile),
 774                     getConfig_CopyrightFile(params));
 775         } else {
 776             try (Writer w = Files.newBufferedWriter(
 777                     getConfig_CopyrightFile(params).toPath())) {
 778                 String content = preprocessTextResource(
 779                         getConfig_CopyrightFile(params).getName(),
 780                         I18N.getString("resource.copyright-file"),
 781                         DEFAULT_COPYRIGHT_TEMPLATE,
 782                         data,
 783                         VERBOSE.fetchFrom(params),
 784                         RESOURCE_DIR.fetchFrom(params));
 785                 w.write(content);
 786             }
 787         }
 788 
 789         return true;
 790     }
 791 
 792     private Map<String, String> createReplacementData(
 793             Map<String, ? super Object> params) throws IOException {
 794         Map<String, String> data = new HashMap<>();
 795         String launcher = LinuxAppImageBuilder.getLauncherRelativePath(params);
 796 
 797         data.put("APPLICATION_NAME", APP_NAME.fetchFrom(params));
 798         data.put("APPLICATION_FS_NAME",
 799                 getConfig_RootDirectory(params).getName());
 800         data.put("APPLICATION_PACKAGE", BUNDLE_NAME.fetchFrom(params));
 801         data.put("APPLICATION_VENDOR", VENDOR.fetchFrom(params));
 802         data.put("APPLICATION_MAINTAINER", MAINTAINER.fetchFrom(params));
 803         data.put("APPLICATION_VERSION", VERSION.fetchFrom(params));
 804         data.put("APPLICATION_RELEASE", RELEASE.fetchFrom(params));
 805         data.put("APPLICATION_SECTION", SECTION.fetchFrom(params));
 806         data.put("APPLICATION_LAUNCHER_FILENAME", launcher);
 807         data.put("INSTALLATION_DIRECTORY", LINUX_INSTALL_DIR.fetchFrom(params));
 808         data.put("XDG_PREFIX", XDG_FILE_PREFIX.fetchFrom(params));
 809         data.put("DEPLOY_BUNDLE_CATEGORY", MENU_GROUP.fetchFrom(params));
 810         data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params));
 811         data.put("APPLICATION_COPYRIGHT", COPYRIGHT.fetchFrom(params));
 812         data.put("APPLICATION_LICENSE_TEXT", LICENSE_TEXT.fetchFrom(params));
 813         data.put("APPLICATION_ARCH", getDebArch());
 814         data.put("APPLICATION_INSTALLED_SIZE",
 815                 Long.toString(getInstalledSizeKB(params)));
 816         data.put("PACKAGE_DEPENDENCIES", LINUX_PACKAGE_DEPENDENCIES.fetchFrom(
 817                 params));
 818         data.put("RUNTIME_INSTALLER", "" +
 819                 StandardBundlerParam.isRuntimeInstaller(params));
 820 
 821         return data;
 822     }
 823 
 824     private File getConfig_DesktopShortcutFile(File rootDir,
 825             Map<String, ? super Object> params) {
 826         return new File(rootDir, APP_NAME.fetchFrom(params) + ".desktop");
 827     }
 828 
 829     private File getConfig_IconFile(File rootDir,
 830             Map<String, ? super Object> params) {
 831         return new File(rootDir, APP_NAME.fetchFrom(params) + ".png");
 832     }
 833 
 834     private File getConfig_ControlFile(Map<String, ? super Object> params) {
 835         return new File(CONFIG_DIR.fetchFrom(params), "control");
 836     }
 837 
 838     private File getConfig_PreinstallFile(Map<String, ? super Object> params) {
 839         return new File(CONFIG_DIR.fetchFrom(params), "preinst");
 840     }
 841 
 842     private File getConfig_PrermFile(Map<String, ? super Object> params) {
 843         return new File(CONFIG_DIR.fetchFrom(params), "prerm");
 844     }
 845 
 846     private File getConfig_PostinstallFile(Map<String, ? super Object> params) {
 847         return new File(CONFIG_DIR.fetchFrom(params), "postinst");
 848     }
 849 
 850     private File getConfig_PostrmFile(Map<String, ? super Object> params) {
 851         return new File(CONFIG_DIR.fetchFrom(params), "postrm");
 852     }
 853 
 854     private File getConfig_CopyrightFile(Map<String, ? super Object> params) {
 855         return Path.of(DEB_IMAGE_DIR.fetchFrom(params).getAbsolutePath(), "usr",
 856                 "share", "doc", BUNDLE_NAME.fetchFrom(params), "copyright").toFile();
 857     }
 858 
 859     private File getConfig_RootDirectory(
 860             Map<String, ? super Object> params) {
 861         return Path.of(APP_IMAGE_ROOT.fetchFrom(params).getAbsolutePath(),
 862                 BUNDLE_NAME.fetchFrom(params)).toFile();
 863     }
 864 
 865     private File buildDeb(Map<String, ? super Object> params,
 866             File outdir) throws IOException {
 867         File outFile = new File(outdir,
 868                 FULL_PACKAGE_NAME.fetchFrom(params)+".deb");
 869         Log.verbose(MessageFormat.format(I18N.getString(
 870                 "message.outputting-to-location"), outFile.getAbsolutePath()));
 871 
 872         outFile.getParentFile().mkdirs();
 873 
 874         // run dpkg
 875         ProcessBuilder pb = new ProcessBuilder(
 876                 "fakeroot", TOOL_DPKG_DEB, "-b",
 877                 FULL_PACKAGE_NAME.fetchFrom(params),
 878                 outFile.getAbsolutePath());
 879         pb = pb.directory(DEB_IMAGE_DIR.fetchFrom(params).getParentFile());
 880         IOUtils.exec(pb);
 881 
 882         Log.verbose(MessageFormat.format(I18N.getString(
 883                 "message.output-to-location"), outFile.getAbsolutePath()));
 884 
 885         return outFile;
 886     }
 887 
 888     @Override
 889     public String getName() {
 890         return I18N.getString("deb.bundler.name");
 891     }
 892 
 893     @Override
 894     public String getID() {
 895         return "deb";
 896     }
 897 
 898     @Override
 899     public String getBundleType() {
 900         return "INSTALLER";
 901     }
 902 
 903     @Override
 904     public File execute(Map<String, ? super Object> params,
 905             File outputParentDir) throws PackagerException {
 906         return bundle(params, outputParentDir);
 907     }
 908 
 909     @Override
 910     public boolean supported(boolean runtimeInstaller) {
 911         return isSupported();
 912     }
 913 
 914     public static boolean isSupported() {
 915         if (Platform.getPlatform() == Platform.LINUX) {
 916             if (testTool(TOOL_DPKG_DEB, "1")) {
 917                 return true;
 918             }
 919         }
 920         return false;
 921     }
 922 
 923     public int getSquareSizeOfImage(File f) {
 924         try {
 925             BufferedImage bi = ImageIO.read(f);
 926             if (bi.getWidth() == bi.getHeight()) {
 927                 return bi.getWidth();
 928             } else {
 929                 return 0;
 930             }
 931         } catch (Exception e) {
 932             Log.verbose(e);
 933             return 0;
 934         }
 935     }
 936 }