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