1 /*
   2  * Copyright (c) 2012, 2014, 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 com.oracle.tools.packager.linux;
  27 
  28 import com.oracle.tools.packager.*;
  29 
  30 import javax.imageio.ImageIO;
  31 import java.awt.image.BufferedImage;
  32 import java.io.*;
  33 import java.nio.file.Files;
  34 import java.nio.file.attribute.PosixFilePermission;
  35 import java.nio.file.attribute.PosixFilePermissions;
  36 import java.text.MessageFormat;
  37 import java.util.*;
  38 import java.util.logging.Level;
  39 import java.util.logging.Logger;
  40 import java.util.regex.Matcher;
  41 import java.util.regex.Pattern;
  42 
  43 import static com.oracle.tools.packager.StandardBundlerParam.*;
  44 
  45 public class LinuxRpmBundler extends AbstractBundler {
  46 
  47     private static final ResourceBundle I18N =
  48             ResourceBundle.getBundle(LinuxRpmBundler.class.getName());
  49 
  50     public static final BundlerParamInfo<LinuxAppBundler> APP_BUNDLER = new StandardBundlerParam<>(
  51             I18N.getString("param.app-bundler.name"), 
  52             I18N.getString("param.app-bundler.description"),
  53             "linux.app.bundler",
  54             LinuxAppBundler.class,
  55             params -> new LinuxAppBundler(),
  56             null);
  57 
  58     public static final BundlerParamInfo<File> RPM_IMAGE_DIR = new StandardBundlerParam<>(
  59             I18N.getString("param.image-dir.name"), 
  60             I18N.getString("param.image-dir.description"),
  61             "linux.rpm.imageDir",
  62             File.class,
  63             params -> {
  64                 File imagesRoot = IMAGES_ROOT.fetchFrom(params);
  65                 if (!imagesRoot.exists()) imagesRoot.mkdirs();
  66                 return new File(imagesRoot, "linux-rpm.image");
  67             },
  68             (s, p) -> new File(s));
  69 
  70     public static final BundlerParamInfo<File> CONFIG_ROOT = new StandardBundlerParam<>(
  71             I18N.getString("param.config-root.name"), 
  72             I18N.getString("param.config-root.description"),
  73             "configRoot",
  74             File.class,
  75             params ->  new File(BUILD_ROOT.fetchFrom(params), "linux"),
  76             (s, p) -> new File(s));
  77 
  78     // Fedora rules for package naming are used here
  79     // https://fedoraproject.org/wiki/Packaging:NamingGuidelines?rd=Packaging/NamingGuidelines
  80     //
  81     // all Fedora packages must be named using only the following ASCII characters.
  82     // These characters are displayed here:
  83     //
  84     // abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._+
  85     //
  86     private static final Pattern RPM_BUNDLE_NAME_PATTERN =
  87             Pattern.compile("[a-z\\d\\+\\-\\.\\_]+", Pattern.CASE_INSENSITIVE);
  88 
  89     public static final BundlerParamInfo<String> BUNDLE_NAME = new StandardBundlerParam<> (
  90             I18N.getString("param.bundle-name.name"), 
  91             I18N.getString("param.bundle-name.description"),
  92             "linux.bundleName",
  93             String.class,
  94             params -> {
  95                 String nm = APP_NAME.fetchFrom(params);
  96                 if (nm == null) return null;
  97 
  98                 // make sure to lower case and spaces become dashes
  99                 nm = nm.toLowerCase().replaceAll("[ ]", "-");
 100 
 101                 return nm;
 102             },
 103             (s, p) -> {
 104                 if (!RPM_BUNDLE_NAME_PATTERN.matcher(s).matches()) {
 105                     throw new IllegalArgumentException(
 106                             new ConfigException(
 107                                 MessageFormat.format(I18N.getString("error.invalid-value-for-package-name"), s),
 108                                                      I18N.getString("error.invalid-value-for-package-name.advice")));
 109                 }
 110 
 111                 return s;
 112             }
 113         );
 114 
 115     public static final BundlerParamInfo<String> XDG_FILE_PREFIX = new StandardBundlerParam<> (
 116             I18N.getString("param.xdg-prefix.name"),
 117             I18N.getString("param.xdg-prefix.description"),
 118             "linux.xdg-prefix",
 119             String.class,
 120             params -> {
 121                 try {
 122                     String vendor;
 123                     if (params.containsKey(VENDOR.getID())) {
 124                         vendor = VENDOR.fetchFrom(params);
 125                     } else {
 126                         vendor = "javapackager";
 127                     }
 128                     String appName = APP_FS_NAME.fetchFrom(params);
 129 
 130                     return (vendor + "-" + appName).replaceAll("\\s", "");
 131                 } catch (Exception e) {
 132                     if (Log.isDebug()) {
 133                         e.printStackTrace();
 134                     }
 135                 }
 136                 return "unknown-MimeInfo.xml";
 137             },
 138             (s, p) -> s);
 139 
 140     private final static String DEFAULT_ICON = "javalogo_white_32.png";
 141     private final static String DEFAULT_SPEC_TEMPLATE = "template.spec";
 142     private final static String DEFAULT_DESKTOP_FILE_TEMPLATE = "template.desktop";
 143     private final static String DEFAULT_INIT_SCRIPT_TEMPLATE = "template.rpm.init.script";
 144 
 145     public final static String TOOL_RPMBUILD = "rpmbuild";
 146     public final static double TOOL_RPMBUILD_MIN_VERSION = 4.0d;
 147 
 148     public LinuxRpmBundler() {
 149         super();
 150         baseResourceLoader = LinuxResources.class;
 151     }
 152 
 153     public static boolean testTool(String toolName, double minVersion) {
 154         try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); PrintStream ps = new PrintStream(baos)) {
 155             ProcessBuilder pb = new ProcessBuilder(
 156                     toolName,
 157                     "--version");
 158 
 159             IOUtils.exec(pb, Log.isDebug(), false, ps); //not interested in the output
 160 
 161             //TODO: Version is ignored; need to extract version string and compare!
 162             String content = new String(baos.toByteArray());
 163             Pattern pattern = Pattern.compile(" (\\d+\\.\\d+)");
 164             Matcher matcher = pattern.matcher(content);
 165             if (matcher.find()) {
 166                 String v = matcher.group(1);
 167                 double version = new Double(v);
 168                 return minVersion <= version;
 169             } else {
 170                return false;
 171             }
 172         } catch (Exception e) {
 173             Log.verbose(MessageFormat.format(I18N.getString("message.test-for-tool"), toolName, e.getMessage()));
 174             return false;
 175         }
 176     }
 177 
 178     @Override
 179     public boolean validate(Map<String, ? super Object> p) throws UnsupportedPlatformException, ConfigException {
 180         try {
 181             if (p == null) throw new ConfigException(
 182                     I18N.getString("error.parameters-null"),
 183                     I18N.getString("error.parameters-null.advice"));
 184 
 185             //run basic validation to ensure requirements are met
 186             //we are not interested in return code, only possible exception
 187             APP_BUNDLER.fetchFrom(p).doValidate(p);
 188 
 189             // validate license file, if used, exists in the proper place
 190             if (p.containsKey(LICENSE_FILE.getID())) {
 191                 List<RelativeFileSet> appResourcesList = APP_RESOURCES_LIST.fetchFrom(p);
 192                 for (String license : LICENSE_FILE.fetchFrom(p)) {
 193                     boolean found = false;
 194                     for (RelativeFileSet appResources : appResourcesList) {
 195                         found = found || appResources.contains(license);
 196                     }
 197                     if (!found) {
 198                         throw new ConfigException(
 199                                 I18N.getString("error.license-missing"),
 200                                 MessageFormat.format(I18N.getString("error.license-missing.advice"),
 201                                         license));
 202                     }
 203                 }
 204             }
 205 
 206             //validate presense of required tools
 207             if (!testTool(TOOL_RPMBUILD, TOOL_RPMBUILD_MIN_VERSION)){
 208                 throw new ConfigException(
 209                         I18N.getString(MessageFormat.format("error.cannot-find-rpmbuild", TOOL_RPMBUILD_MIN_VERSION)),
 210                         I18N.getString(MessageFormat.format("error.cannot-find-rpmbuild.advice", TOOL_RPMBUILD_MIN_VERSION)));
 211             }
 212 
 213             //treat default null as "system wide install"
 214             boolean systemWide = SYSTEM_WIDE.fetchFrom(p) == null || SYSTEM_WIDE.fetchFrom(p);
 215             boolean serviceHint = p.containsKey(SERVICE_HINT.getID()) && SERVICE_HINT.fetchFrom(p);
 216 
 217             if (serviceHint && !systemWide) {
 218                 throw new ConfigException(
 219                         I18N.getString("error.no-support-for-peruser-daemons"),
 220                         I18N.getString("error.no-support-for-peruser-daemons.advice"));
 221             }
 222             
 223             // only one mime type per association, at least one file extension
 224             List<Map<String, ? super Object>> associations = FILE_ASSOCIATIONS.fetchFrom(p);
 225             if (associations != null) {
 226                 for (int i = 0; i < associations.size(); i++) {
 227                     Map<String, ? super Object> assoc = associations.get(i);
 228                     List<String> mimes = FA_CONTENT_TYPE.fetchFrom(assoc);
 229                     if (mimes == null || mimes.isEmpty()) {
 230                         throw new ConfigException(
 231                                 MessageFormat.format(I18N.getString("error.no-content-types-for-file-association"), i),
 232                                 I18N.getString("error.no-content-types-for-file-association.advice"));
 233                     } else if (mimes.size() > 1) {
 234                         throw new ConfigException(
 235                                 MessageFormat.format(I18N.getString("error.too-many-content-types-for-file-association"), i),
 236                                 I18N.getString("error.too-many-content-types-for-file-association.advice"));
 237                     }
 238                 }
 239             }
 240 
 241             // bundle name has some restrictions
 242             // the string converter will throw an exception if invalid
 243             BUNDLE_NAME.getStringConverter().apply(BUNDLE_NAME.fetchFrom(p), p);
 244 
 245             return true;
 246         } catch (RuntimeException re) {
 247             if (re.getCause() instanceof ConfigException) {
 248                 throw (ConfigException) re.getCause();
 249             } else {
 250                 throw new ConfigException(re);
 251             }
 252         }
 253     }
 254 
 255     private boolean prepareProto(Map<String, ? super Object> params) {
 256         File imageDir = RPM_IMAGE_DIR.fetchFrom(params);
 257         File appDir = APP_BUNDLER.fetchFrom(params).doBundle(params, imageDir, true);
 258         return appDir != null;
 259     }
 260 
 261     public File bundle(Map<String, ? super Object> p, File outdir) {
 262         if (!outdir.isDirectory() && !outdir.mkdirs()) {
 263             throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-create-output-dir"), outdir.getAbsolutePath()));
 264         }
 265         if (!outdir.canWrite()) {
 266             throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-write-to-output-dir"), outdir.getAbsolutePath()));
 267         }
 268 
 269         File imageDir = RPM_IMAGE_DIR.fetchFrom(p);
 270         try {
 271 
 272             imageDir.mkdirs();
 273 
 274             boolean menuShortcut = MENU_HINT.fetchFrom(p);
 275             boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(p);
 276             if (!menuShortcut && !desktopShortcut) {
 277                 //both can not be false - user will not find the app
 278                 Log.verbose(I18N.getString("message.one-shortcut-required"));
 279                 p.put(MENU_HINT.getID(), true);
 280             }
 281 
 282             if (prepareProto(p) && prepareProjectConfig(p)) {
 283                 return buildRPM(p, outdir);
 284             }
 285             return null;
 286         } catch (IOException ex) {
 287             ex.printStackTrace();
 288             return null;
 289         } finally {
 290             try {
 291                 if (VERBOSE.fetchFrom(p)) {
 292                     saveConfigFiles(p);
 293                 }
 294                 if (imageDir != null && !Log.isDebug()) {
 295                     IOUtils.deleteRecursive(imageDir);
 296                 } else if (imageDir != null) {
 297                     Log.info(MessageFormat.format(I18N.getString("message.debug-working-directory"), imageDir.getAbsolutePath()));
 298                 }
 299             } catch (FileNotFoundException ex) {
 300                 //noinspection ReturnInsideFinallyBlock
 301                 return null;
 302             }
 303         }
 304     }
 305 
 306     /*
 307      * set permissions with a string like "rwxr-xr-x"
 308      * 
 309      * This cannot be directly backport to 22u which is unfortunately built with 1.6
 310      */
 311     private void setPermissions(File file, String permissions) {
 312         Set<PosixFilePermission> filePermissions = PosixFilePermissions.fromString(permissions);
 313         try {
 314             if (file.exists()) {
 315                 Files.setPosixFilePermissions(file.toPath(), filePermissions);
 316             }
 317         } catch (IOException ex) {
 318             Logger.getLogger(LinuxDebBundler.class.getName()).log(Level.SEVERE, null, ex);
 319         }
 320     }
 321     
 322     protected void saveConfigFiles(Map<String, ? super Object> params) {
 323         try {
 324             File configRoot = CONFIG_ROOT.fetchFrom(params);
 325             File rootDir = LinuxAppBundler.getRootDir(RPM_IMAGE_DIR.fetchFrom(params), params);
 326 
 327             if (getConfig_SpecFile(params).exists()) {
 328                 IOUtils.copyFile(getConfig_SpecFile(params),
 329                         new File(configRoot, getConfig_SpecFile(params).getName()));
 330             }
 331             if (getConfig_DesktopShortcutFile(rootDir, params).exists()) {
 332                 IOUtils.copyFile(getConfig_DesktopShortcutFile(rootDir, params),
 333                         new File(configRoot, getConfig_DesktopShortcutFile(rootDir, params).getName()));
 334             }
 335             if (getConfig_IconFile(rootDir, params).exists()) {
 336                 IOUtils.copyFile(getConfig_IconFile(rootDir, params),
 337                         new File(configRoot, getConfig_IconFile(rootDir, params).getName()));
 338             }
 339             if (SERVICE_HINT.fetchFrom(params)) {
 340                 if (getConfig_InitScriptFile(params).exists()) {
 341                     IOUtils.copyFile(getConfig_InitScriptFile(params),
 342                             new File(configRoot, getConfig_InitScriptFile(params).getName()));
 343                 }
 344             }
 345             Log.info(MessageFormat.format(I18N.getString("message.config-save-location"), configRoot.getAbsolutePath()));
 346         } catch (IOException ioe) {
 347             ioe.printStackTrace();
 348         }
 349     }
 350 
 351     private String getLicenseFileString(Map<String, ? super Object> params) {
 352         StringBuilder sb = new StringBuilder();
 353         for (String f: LICENSE_FILE.fetchFrom(params)) {
 354             if (sb.length() != 0) {
 355                 sb.append("\n");
 356             }
 357             sb.append("%doc /opt/");
 358             sb.append(APP_FS_NAME.fetchFrom(params));
 359             sb.append("/app/");
 360             sb.append(f);
 361         }
 362         return sb.toString();
 363     }
 364 
 365     private boolean prepareProjectConfig(Map<String, ? super Object> params) throws IOException {
 366         Map<String, String> data = createReplacementData(params);
 367         File rootDir = LinuxAppBundler.getRootDir(RPM_IMAGE_DIR.fetchFrom(params), params);
 368 
 369         //prepare installer icon
 370         File iconTarget = getConfig_IconFile(rootDir, params);
 371         File icon = LinuxAppBundler.ICON_PNG.fetchFrom(params);
 372         if (icon == null || !icon.exists()) {
 373             fetchResource(LinuxAppBundler.LINUX_BUNDLER_PREFIX + iconTarget.getName(),
 374                     I18N.getString("resource.menu-icon"),
 375                     DEFAULT_ICON,
 376                     iconTarget,
 377                     VERBOSE.fetchFrom(params),
 378                     DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 379         } else {
 380             fetchResource(LinuxAppBundler.LINUX_BUNDLER_PREFIX + iconTarget.getName(),
 381                     I18N.getString("resource.menu-icon"),
 382                     icon,
 383                     iconTarget,
 384                     VERBOSE.fetchFrom(params),
 385                     DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 386         }
 387 
 388         StringBuilder installScripts = new StringBuilder();
 389         StringBuilder removeScripts = new StringBuilder();
 390         for (Map<String, ? super Object> secondaryLauncher : SECONDARY_LAUNCHERS.fetchFrom(params)) {
 391             Map<String, String> secondaryLauncherData = createReplacementData(secondaryLauncher);
 392             secondaryLauncherData.put("APPLICATION_FS_NAME", data.get("APPLICATION_FS_NAME"));
 393             secondaryLauncherData.put("DESKTOP_MIMES", "");
 394 
 395             //prepare desktop shortcut
 396             Writer w = new BufferedWriter(new FileWriter(getConfig_DesktopShortcutFile(rootDir, secondaryLauncher)));
 397             String content = preprocessTextResource(
 398                     LinuxAppBundler.LINUX_BUNDLER_PREFIX + getConfig_DesktopShortcutFile(rootDir, secondaryLauncher).getName(),
 399                     I18N.getString("resource.menu-shortcut-descriptor"), DEFAULT_DESKTOP_FILE_TEMPLATE, secondaryLauncherData,
 400                     VERBOSE.fetchFrom(params),
 401                     DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 402             w.write(content);
 403             w.close();
 404 
 405             //prepare installer icon
 406             iconTarget = getConfig_IconFile(rootDir, secondaryLauncher);
 407             icon = LinuxAppBundler.ICON_PNG.fetchFrom(secondaryLauncher);
 408             if (icon == null || !icon.exists()) {
 409                 fetchResource(LinuxAppBundler.LINUX_BUNDLER_PREFIX + iconTarget.getName(),
 410                         I18N.getString("resource.menu-icon"),
 411                         DEFAULT_ICON,
 412                         iconTarget,
 413                         VERBOSE.fetchFrom(params),
 414                         DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 415             } else {
 416                 fetchResource(LinuxAppBundler.LINUX_BUNDLER_PREFIX + iconTarget.getName(),
 417                         I18N.getString("resource.menu-icon"),
 418                         icon,
 419                         iconTarget,
 420                         VERBOSE.fetchFrom(params),
 421                         DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 422             }
 423 
 424             //post copying of desktop icon
 425             installScripts.append("xdg-desktop-menu install --novendor /opt/");
 426             installScripts.append(data.get("APPLICATION_FS_NAME"));
 427             installScripts.append("/");
 428             installScripts.append(secondaryLauncherData.get("APPLICATION_LAUNCHER_FILENAME"));
 429             installScripts.append(".desktop\n");
 430 
 431             //preun cleanup of desktop icon
 432             removeScripts.append("xdg-desktop-menu uninstall --novendor /opt/");
 433             removeScripts.append(data.get("APPLICATION_FS_NAME"));
 434             removeScripts.append("/");
 435             removeScripts.append(secondaryLauncherData.get("APPLICATION_LAUNCHER_FILENAME"));
 436             removeScripts.append(".desktop\n");
 437 
 438         }
 439         data.put("SECONDARY_LAUNCHERS_INSTALL", installScripts.toString());
 440         data.put("SECONDARY_LAUNCHERS_REMOVE", removeScripts.toString());
 441 
 442         List<Map<String, ? super Object>> associations = FILE_ASSOCIATIONS.fetchFrom(params);
 443         data.put("FILE_ASSOCIATION_INSTALL", "");
 444         data.put("FILE_ASSOCIATION_REMOVE", "");
 445         data.put("DESKTOP_MIMES", "");
 446         if (associations != null) {
 447             String mimeInfoFile = XDG_FILE_PREFIX.fetchFrom(params) + "-MimeInfo.xml";
 448             StringBuilder mimeInfo = new StringBuilder("<?xml version=\"1.0\"?>\n<mime-info xmlns='http://www.freedesktop.org/standards/shared-mime-info'>\n");
 449             StringBuilder registrations = new StringBuilder();
 450             StringBuilder deregistrations = new StringBuilder();
 451             StringBuilder desktopMimes = new StringBuilder("MimeType=");
 452             boolean addedEntry = false;
 453 
 454             for (Map<String, ? super Object> assoc : associations) {
 455                 //  <mime-type type="application/x-vnd.awesome">
 456                 //    <comment>Awesome document</comment>
 457                 //    <glob pattern="*.awesome"/>
 458                 //    <glob pattern="*.awe"/>
 459                 //  </mime-type>
 460 
 461                 if (assoc == null) {
 462                     continue;
 463                 }
 464 
 465                 String description = FA_DESCRIPTION.fetchFrom(assoc);
 466                 File faIcon = FA_ICON.fetchFrom(assoc); //TODO FA_ICON_PNG
 467                 List<String> extensions = FA_EXTENSIONS.fetchFrom(assoc);
 468                 if (extensions == null) {
 469                     Log.info(I18N.getString("message.creating-association-with-null-extension"));
 470                 }
 471 
 472                 List<String> mimes = FA_CONTENT_TYPE.fetchFrom(assoc);
 473                 if (mimes == null || mimes.isEmpty()) {
 474                     continue;
 475                 }
 476                 String thisMime = mimes.get(0);
 477                 String dashMime = thisMime.replace('/', '-');
 478 
 479                 mimeInfo.append("  <mime-type type='")
 480                         .append(thisMime)
 481                         .append("'>\n");
 482                 if (description != null && !description.isEmpty()) {
 483                     mimeInfo.append("    <comment>")
 484                             .append(description)
 485                             .append("</comment>\n");
 486                 }
 487 
 488                 if (extensions != null) {
 489                     for (String ext : extensions) {
 490                         mimeInfo.append("    <glob pattern='*.")
 491                                 .append(ext)
 492                                 .append("'/>\n");
 493                     }
 494                 }
 495 
 496                 mimeInfo.append("  </mime-type>\n");
 497                 if (!addedEntry) {
 498                     registrations.append("xdg-mime install /opt/")
 499                             .append(data.get("APPLICATION_FS_NAME"))
 500                             .append("/")
 501                             .append(mimeInfoFile)
 502                             .append("\n");
 503 
 504                     deregistrations.append("xdg-mime uninstall /opt/")
 505                             .append(data.get("APPLICATION_FS_NAME"))
 506                             .append("/")
 507                             .append(mimeInfoFile)
 508                             .append("\n");
 509                     addedEntry = true;
 510                 } else {
 511                     desktopMimes.append(";");
 512                 }
 513                 desktopMimes.append(thisMime);
 514 
 515                 if (faIcon != null && faIcon.exists()) {
 516                     int size = getSquareSizeOfImage(faIcon);
 517 
 518                     if (size > 0) {
 519                         File target = new File(rootDir, APP_FS_NAME.fetchFrom(params) + "_fa_" + faIcon.getName());
 520                         IOUtils.copyFile(faIcon, target);
 521 
 522                         //xdg-icon-resource install --context mimetypes --size 64 awesomeapp_fa_1.png application-x.vnd-awesome
 523                         registrations.append("xdg-icon-resource install --context mimetypes --size ")
 524                                 .append(size)
 525                                 .append(" /opt/")
 526                                 .append(data.get("APPLICATION_FS_NAME"))
 527                                 .append("/")
 528                                 .append(target.getName())
 529                                 .append(" ")
 530                                 .append(dashMime)
 531                                 .append("\n");
 532 
 533                         //xdg-icon-resource uninstall --context mimetypes --size 64 awesomeapp_fa_1.png application-x.vnd-awesome
 534                         deregistrations.append("xdg-icon-resource uninstall --context mimetypes --size ")
 535                                 .append(size)
 536                                 .append(" /opt/")
 537                                 .append(data.get("APPLICATION_FS_NAME"))
 538                                 .append("/")
 539                                 .append(target.getName())
 540                                 .append(" ")
 541                                 .append(dashMime)
 542                                 .append("\n");
 543                     }
 544                 }
 545             }
 546             mimeInfo.append("</mime-info>");
 547 
 548             if (addedEntry) {
 549                 Writer w = new BufferedWriter(new FileWriter(new File(rootDir, mimeInfoFile)));
 550                 w.write(mimeInfo.toString());
 551                 w.close();
 552                 data.put("FILE_ASSOCIATION_INSTALL", registrations.toString());
 553                 data.put("FILE_ASSOCIATION_REMOVE", deregistrations.toString());
 554                 data.put("DESKTOP_MIMES", desktopMimes.toString());
 555             }
 556         }
 557         //prepare desktop shortcut
 558         Writer w = new BufferedWriter(new FileWriter(getConfig_DesktopShortcutFile(rootDir, params)));
 559         String content = preprocessTextResource(
 560                 LinuxAppBundler.LINUX_BUNDLER_PREFIX + getConfig_DesktopShortcutFile(rootDir, params).getName(),
 561                 I18N.getString("resource.menu-shortcut-descriptor"), DEFAULT_DESKTOP_FILE_TEMPLATE, data,
 562                 VERBOSE.fetchFrom(params),
 563                 DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 564         w.write(content);
 565         w.close();
 566 
 567         //prepare spec file
 568         w = new BufferedWriter(new FileWriter(getConfig_SpecFile(params)));
 569         content = preprocessTextResource(
 570                 LinuxAppBundler.LINUX_BUNDLER_PREFIX + getConfig_SpecFile(params).getName(),
 571                 I18N.getString("resource.rpm-spec-file"), DEFAULT_SPEC_TEMPLATE, data,
 572                 VERBOSE.fetchFrom(params),
 573                 DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 574         w.write(content);
 575         w.close();
 576 
 577         if (SERVICE_HINT.fetchFrom(params)) {
 578             //prepare init script
 579             w = new BufferedWriter(new FileWriter(getConfig_InitScriptFile(params)));
 580             content = preprocessTextResource(
 581                     LinuxAppBundler.LINUX_BUNDLER_PREFIX + getConfig_InitScriptFile(params).getName(),
 582                     I18N.getString("resource.rpm-init-script"), 
 583                     DEFAULT_INIT_SCRIPT_TEMPLATE, 
 584                     data,
 585                     VERBOSE.fetchFrom(params),
 586                     DROP_IN_RESOURCES_ROOT.fetchFrom(params));
 587             w.write(content);
 588             w.close();
 589             setPermissions(getConfig_InitScriptFile(params), "rwxr-xr-x");
 590         }
 591 
 592         return true;
 593     }
 594 
 595     private Map<String, String> createReplacementData(Map<String, ? super Object> params) {
 596         Map<String, String> data = new HashMap<>();
 597 
 598         data.put("APPLICATION_NAME", APP_NAME.fetchFrom(params));
 599         data.put("APPLICATION_FS_NAME", APP_FS_NAME.fetchFrom(params));
 600         data.put("APPLICATION_PACKAGE", BUNDLE_NAME.fetchFrom(params));
 601         data.put("APPLICATION_VENDOR", VENDOR.fetchFrom(params));
 602         data.put("APPLICATION_VERSION", VERSION.fetchFrom(params));
 603         data.put("APPLICATION_LAUNCHER_FILENAME", APP_FS_NAME.fetchFrom(params));
 604         data.put("XDG_PREFIX", XDG_FILE_PREFIX.fetchFrom(params));
 605         data.put("DEPLOY_BUNDLE_CATEGORY", CATEGORY.fetchFrom(params)); //TODO rpm categories
 606         data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params));
 607         data.put("APPLICATION_SUMMARY", TITLE.fetchFrom(params));
 608         data.put("APPLICATION_LICENSE_TYPE", LICENSE_TYPE.fetchFrom(params));
 609         data.put("APPLICATION_LICENSE_FILE", getLicenseFileString(params));
 610         data.put("SERVICE_HINT", String.valueOf(SERVICE_HINT.fetchFrom(params)));
 611         data.put("START_ON_INSTALL", String.valueOf(START_ON_INSTALL.fetchFrom(params)));
 612         data.put("STOP_ON_UNINSTALL", String.valueOf(STOP_ON_UNINSTALL.fetchFrom(params)));
 613         data.put("RUN_AT_STARTUP", String.valueOf(RUN_AT_STARTUP.fetchFrom(params)));
 614         return data;
 615     }
 616 
 617     private File getConfig_DesktopShortcutFile(File rootDir, Map<String, ? super Object> params) {
 618         return new File(rootDir,
 619                 APP_FS_NAME.fetchFrom(params) + ".desktop");
 620     }
 621 
 622     private File getConfig_IconFile(File rootDir, Map<String, ? super Object> params) {
 623         return new File(rootDir,
 624                 APP_FS_NAME.fetchFrom(params) + ".png");
 625     }
 626 
 627     private File getConfig_InitScriptFile(Map<String, ? super Object> params) {
 628         return new File(LinuxAppBundler.getRootDir(RPM_IMAGE_DIR.fetchFrom(params), params),
 629                 BUNDLE_NAME.fetchFrom(params) + ".init");
 630     }
 631 
 632     private File getConfig_SpecFile(Map<String, ? super Object> params) {
 633         return new File(RPM_IMAGE_DIR.fetchFrom(params),
 634                 APP_FS_NAME.fetchFrom(params) + ".spec");
 635     }
 636 
 637     private File buildRPM(Map<String, ? super Object> params, File outdir) throws IOException {
 638         Log.verbose(MessageFormat.format(I18N.getString("message.outputting-bundle-location"), outdir.getAbsolutePath()));
 639 
 640         File broot = new File(BUILD_ROOT.fetchFrom(params), "rmpbuildroot");
 641 
 642         outdir.mkdirs();
 643 
 644         //run rpmbuild
 645         ProcessBuilder pb = new ProcessBuilder(
 646                 TOOL_RPMBUILD,
 647                 "-bb", getConfig_SpecFile(params).getAbsolutePath(),
 648 //                "--define", "%__jar_repack %{nil}",  //debug: improves build time (but will require unpack to install?)
 649                 "--define", "%_sourcedir "+ RPM_IMAGE_DIR.fetchFrom(params).getAbsolutePath(),
 650                 "--define", "%_rpmdir " + outdir.getAbsolutePath(), //save result to output dir
 651                 "--define", "%_topdir " + broot.getAbsolutePath() //do not use other system directories to build as current user
 652         );
 653         pb = pb.directory(RPM_IMAGE_DIR.fetchFrom(params));
 654         IOUtils.exec(pb, VERBOSE.fetchFrom(params));
 655 
 656         if (!Log.isDebug()) {
 657             IOUtils.deleteRecursive(broot);
 658         }
 659 
 660         Log.info(MessageFormat.format(I18N.getString("message.output-bundle-location"), outdir.getAbsolutePath()));
 661 
 662         // presume the result is the ".rpm" file with the newest modified time
 663         // not the best solution, but it is the most reliable
 664         File result = null;
 665         long lastModified = 0;
 666         File[] list = outdir.listFiles();
 667         if (list != null) {
 668             for (File f : list) {
 669                 if (f.getName().endsWith(".rpm") && f.lastModified() > lastModified) {
 670                     result = f;
 671                     lastModified = f.lastModified();
 672                 }
 673             }
 674         }
 675 
 676         return result;
 677     }
 678 
 679     @Override
 680     public String getName() {
 681         return I18N.getString("bundler.name");
 682     }
 683 
 684     @Override
 685     public String getDescription() {
 686         return I18N.getString("bundler.description");
 687     }
 688 
 689     @Override
 690     public String getID() {
 691         return "rpm";
 692     }
 693 
 694     @Override
 695     public String getBundleType() {
 696         return "INSTALLER";
 697     }
 698 
 699     @Override
 700     public Collection<BundlerParamInfo<?>> getBundleParameters() {
 701         Collection<BundlerParamInfo<?>> results = new LinkedHashSet<>();
 702         results.addAll(LinuxAppBundler.getAppBundleParameters());
 703         results.addAll(getRpmBundleParameters());
 704         return results;
 705     }
 706 
 707     public static Collection<BundlerParamInfo<?>> getRpmBundleParameters() {
 708         return Arrays.asList(
 709                 BUNDLE_NAME,
 710                 CATEGORY,
 711                 DESCRIPTION,
 712                 LinuxAppBundler.ICON_PNG,
 713                 LICENSE_FILE,
 714                 LICENSE_TYPE,
 715                 TITLE,
 716                 VENDOR
 717         );
 718     }
 719 
 720     @Override
 721     public File execute(Map<String, ? super Object> params, File outputParentDir) {
 722         return bundle(params, outputParentDir);
 723     }
 724 
 725 
 726     public int getSquareSizeOfImage(File f) {
 727         try {
 728             BufferedImage bi = ImageIO.read(f);
 729             if (bi.getWidth() == bi.getHeight()) {
 730                 return bi.getWidth();
 731             } else {
 732                 return 0;
 733             }
 734         } catch (Exception e) {
 735             e.printStackTrace();
 736             return 0;
 737         }
 738     }
 739 }