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