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