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