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