8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package jdk.jpackage.internal; 27 28 import javax.imageio.ImageIO; 29 import java.awt.image.BufferedImage; 30 import java.io.*; 31 import java.nio.file.Files; 32 import java.text.MessageFormat; 33 import java.util.*; 34 import java.util.regex.Matcher; 35 import java.util.regex.Pattern; 36 37 import static jdk.jpackage.internal.StandardBundlerParam.*; 38 import static jdk.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR; 39 import static jdk.jpackage.internal.LinuxAppBundler.LINUX_PACKAGE_DEPENDENCIES; 40 41 /** 42 * There are two command line options to configure license information for RPM 43 * packaging: --linux-rpm-license-type and --license-file. Value of 44 * --linux-rpm-license-type command line option configures "License:" section 45 * of RPM spec. Value of --license-file command line option specifies a license 46 * file to be added to the package. License file is a sort of documentation file 47 * but it will be installed even if user selects an option to install the 48 * package without documentation. --linux-rpm-license-type is the primary option 49 * to set license information. --license-file makes little sense in case of RPM 50 * packaging. 51 */ 52 public class LinuxRpmBundler extends AbstractBundler { 53 54 private static final ResourceBundle I18N = ResourceBundle.getBundle( 55 "jdk.jpackage.internal.resources.LinuxResources"); 56 57 public static final BundlerParamInfo<LinuxAppBundler> APP_BUNDLER = 58 new StandardBundlerParam<>( 59 "linux.app.bundler", 60 LinuxAppBundler.class, 61 params -> new LinuxAppBundler(), 62 null); 63 64 public static final BundlerParamInfo<File> RPM_IMAGE_DIR = 65 new StandardBundlerParam<>( 66 "linux.rpm.imageDir", 67 File.class, 68 params -> { 69 File imagesRoot = IMAGES_ROOT.fetchFrom(params); 70 if (!imagesRoot.exists()) imagesRoot.mkdirs(); 71 return new File(imagesRoot, "linux-rpm.image"); 72 }, 73 (s, p) -> new File(s)); 74 75 // Fedora rules for package naming are used here 76 // https://fedoraproject.org/wiki/Packaging:NamingGuidelines?rd=Packaging/NamingGuidelines 77 // 78 // all Fedora packages must be named using only the following ASCII 79 // characters. These characters are displayed here: 80 // 81 // abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._+ 82 // 83 private static final Pattern RPM_BUNDLE_NAME_PATTERN = 84 Pattern.compile("[a-z\\d\\+\\-\\.\\_]+", Pattern.CASE_INSENSITIVE); 85 86 public static final BundlerParamInfo<String> BUNDLE_NAME = 87 new StandardBundlerParam<> ( 88 Arguments.CLIOptions.LINUX_BUNDLE_NAME.getId(), 89 String.class, 90 params -> { 91 String nm = APP_NAME.fetchFrom(params); 92 if (nm == null) return null; 93 94 // make sure to lower case and spaces become dashes 95 nm = nm.toLowerCase().replaceAll("[ ]", "-"); 96 97 return nm; 98 }, 99 (s, p) -> { 100 if (!RPM_BUNDLE_NAME_PATTERN.matcher(s).matches()) { 101 String msgKey = "error.invalid-value-for-package-name"; 102 throw new IllegalArgumentException( 103 new ConfigException(MessageFormat.format( 104 I18N.getString(msgKey), s), 105 I18N.getString(msgKey + ".advice"))); 106 } 107 108 return s; 109 } 110 ); 111 112 public static final BundlerParamInfo<String> MENU_GROUP = 113 new StandardBundlerParam<>( 114 Arguments.CLIOptions.LINUX_MENU_GROUP.getId(), 115 String.class, 116 params -> I18N.getString("param.menu-group.default"), 117 (s, p) -> s 118 ); 119 120 public static final BundlerParamInfo<String> LICENSE_TYPE = 121 new StandardBundlerParam<>( 122 Arguments.CLIOptions.LINUX_RPM_LICENSE_TYPE.getId(), 123 String.class, 124 params -> I18N.getString("param.license-type.default"), 125 (s, p) -> s 126 ); 127 128 public static final BundlerParamInfo<String> GROUP = 129 new StandardBundlerParam<>( 130 Arguments.CLIOptions.LINUX_CATEGORY.getId(), 131 String.class, 132 params -> null, 133 (s, p) -> s); 134 135 public static final BundlerParamInfo<String> XDG_FILE_PREFIX = 136 new StandardBundlerParam<> ( 137 "linux.xdg-prefix", 138 String.class, 139 params -> { 140 try { 141 String vendor; 142 if (params.containsKey(VENDOR.getID())) { 143 vendor = VENDOR.fetchFrom(params); 144 } else { 145 vendor = "jpackage"; 146 } 147 String appName = APP_NAME.fetchFrom(params); 148 149 return (vendor + "-" + appName).replaceAll("\\s", ""); 150 } catch (Exception e) { 151 Log.verbose(e); 152 } 153 return "unknown-MimeInfo.xml"; 154 }, 155 (s, p) -> s); 156 157 public static final StandardBundlerParam<Boolean> SHORTCUT_HINT = 158 new StandardBundlerParam<>( 159 Arguments.CLIOptions.LINUX_SHORTCUT_HINT.getId(), 160 Boolean.class, 161 params -> false, 162 (s, p) -> (s == null || "null".equalsIgnoreCase(s)) 163 ? false : Boolean.valueOf(s) 164 ); 165 166 private final static String DEFAULT_ICON = "java32.png"; 167 private final static String DEFAULT_SPEC_TEMPLATE = "template.spec"; 168 private final static String DEFAULT_DESKTOP_FILE_TEMPLATE = 169 "template.desktop"; 170 171 public final static String TOOL_RPMBUILD = "rpmbuild"; 172 public final static double TOOL_RPMBUILD_MIN_VERSION = 4.0d; 173 174 public static boolean testTool(String toolName, double minVersion) { 175 try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); 176 PrintStream ps = new PrintStream(baos)) { 177 ProcessBuilder pb = new ProcessBuilder(toolName, "--version"); 178 IOUtils.exec(pb, false, ps); 179 //not interested in the above's output 180 String content = new String(baos.toByteArray()); 181 Pattern pattern = Pattern.compile(" (\\d+\\.\\d+)"); 182 Matcher matcher = pattern.matcher(content); 183 184 if (matcher.find()) { 185 String v = matcher.group(1); 186 double version = Double.parseDouble(v); 187 return minVersion <= version; 188 } else { 189 return false; 190 } 191 } catch (Exception e) { 192 Log.verbose(MessageFormat.format(I18N.getString( 193 "message.test-for-tool"), toolName, e.getMessage())); 194 return false; 195 } 196 } 197 198 @Override 199 public boolean validate(Map<String, ? super Object> params) 200 throws ConfigException { 201 try { 202 if (params == null) throw new ConfigException( 203 I18N.getString("error.parameters-null"), 204 I18N.getString("error.parameters-null.advice")); 205 206 // run basic validation to ensure requirements are met 207 // we are not interested in return code, only possible exception 208 APP_BUNDLER.fetchFrom(params).validate(params); 209 210 // validate presense of required tools 211 if (!testTool(TOOL_RPMBUILD, TOOL_RPMBUILD_MIN_VERSION)){ 212 throw new ConfigException( 213 MessageFormat.format( 214 I18N.getString("error.cannot-find-rpmbuild"), 215 TOOL_RPMBUILD_MIN_VERSION), 216 MessageFormat.format( 217 I18N.getString("error.cannot-find-rpmbuild.advice"), 218 TOOL_RPMBUILD_MIN_VERSION)); 219 } 220 221 // only one mime type per association, at least one file extension 222 List<Map<String, ? super Object>> associations = 223 FILE_ASSOCIATIONS.fetchFrom(params); 224 if (associations != null) { 225 for (int i = 0; i < associations.size(); i++) { 226 Map<String, ? super Object> assoc = associations.get(i); 227 List<String> mimes = FA_CONTENT_TYPE.fetchFrom(assoc); 228 if (mimes == null || mimes.isEmpty()) { 229 String msgKey = 230 "error.no-content-types-for-file-association"; 231 throw new ConfigException( 232 MessageFormat.format(I18N.getString(msgKey), i), 233 I18N.getString(msgKey + ".advice")); 234 } else if (mimes.size() > 1) { 235 String msgKey = 236 "error.no-content-types-for-file-association"; 237 throw new ConfigException( 238 MessageFormat.format(I18N.getString(msgKey), i), 239 I18N.getString(msgKey + ".advice")); 240 } 241 } 242 } 243 244 // bundle name has some restrictions 245 // the string converter will throw an exception if invalid 246 BUNDLE_NAME.getStringConverter().apply( 247 BUNDLE_NAME.fetchFrom(params), params); 248 249 return true; 250 } catch (RuntimeException re) { 251 if (re.getCause() instanceof ConfigException) { 252 throw (ConfigException) re.getCause(); 253 } else { 254 throw new ConfigException(re); 255 } 256 } 257 } 258 259 private boolean prepareProto(Map<String, ? super Object> params) 260 throws PackagerException, IOException { 261 File appImage = StandardBundlerParam.getPredefinedAppImage(params); 262 File appDir = null; 263 264 // we either have an application image or need to build one 265 if (appImage != null) { 266 appDir = new File(RPM_IMAGE_DIR.fetchFrom(params), 267 APP_NAME.fetchFrom(params)); 268 // copy everything from appImage dir into appDir/name 269 IOUtils.copyRecursive(appImage.toPath(), appDir.toPath()); 270 } else { 271 appDir = APP_BUNDLER.fetchFrom(params).doBundle(params, 272 RPM_IMAGE_DIR.fetchFrom(params), true); 273 } 274 return appDir != null; 275 } 276 277 public File bundle(Map<String, ? super Object> params, 278 File outdir) throws PackagerException { 279 280 IOUtils.writableOutputDir(outdir.toPath()); 281 282 File imageDir = RPM_IMAGE_DIR.fetchFrom(params); 283 try { 284 285 imageDir.mkdirs(); 286 287 if (prepareProto(params) && prepareProjectConfig(params)) { 288 return buildRPM(params, outdir); 289 } 290 return null; 291 } catch (IOException ex) { 292 Log.verbose(ex); 293 throw new PackagerException(ex); 294 } 295 } 296 297 private boolean prepareProjectConfig(Map<String, ? super Object> params) 298 throws IOException { 299 Map<String, String> data = createReplacementData(params); 300 File rootDir = 301 LinuxAppBundler.getRootDir(RPM_IMAGE_DIR.fetchFrom(params), params); 302 File binDir = new File(rootDir, "bin"); 303 304 // prepare installer icon 305 File iconTarget = getConfig_IconFile(binDir, params); 306 File icon = LinuxAppBundler.ICON_PNG.fetchFrom(params); 307 if (!StandardBundlerParam.isRuntimeInstaller(params)) { 308 if (icon == null || !icon.exists()) { 309 fetchResource(iconTarget.getName(), 310 I18N.getString("resource.menu-icon"), 311 DEFAULT_ICON, 312 iconTarget, 313 VERBOSE.fetchFrom(params), 314 RESOURCE_DIR.fetchFrom(params)); 315 } else { 316 fetchResource(iconTarget.getName(), 317 I18N.getString("resource.menu-icon"), 318 icon, 319 iconTarget, 320 VERBOSE.fetchFrom(params), 321 RESOURCE_DIR.fetchFrom(params)); 322 } 323 } 324 325 StringBuilder installScripts = new StringBuilder(); 326 StringBuilder removeScripts = new StringBuilder(); 327 for (Map<String, ? super Object> addLauncher : 328 ADD_LAUNCHERS.fetchFrom(params)) { 329 Map<String, String> addLauncherData = 330 createReplacementData(addLauncher); 331 addLauncherData.put("APPLICATION_FS_NAME", 332 data.get("APPLICATION_FS_NAME")); 333 addLauncherData.put("DESKTOP_MIMES", ""); 334 335 // prepare desktop shortcut 336 if (SHORTCUT_HINT.fetchFrom(params)) { 337 try (Writer w = Files.newBufferedWriter( 338 getConfig_DesktopShortcutFile(binDir, 339 addLauncher).toPath())) { 340 String content = preprocessTextResource( 341 getConfig_DesktopShortcutFile(binDir, 342 addLauncher).getName(), 343 I18N.getString("resource.menu-shortcut-descriptor"), 344 DEFAULT_DESKTOP_FILE_TEMPLATE, addLauncherData, 345 VERBOSE.fetchFrom(params), 346 RESOURCE_DIR.fetchFrom(params)); 347 w.write(content); 348 } 349 } 350 351 // prepare installer icon 352 iconTarget = getConfig_IconFile(binDir, addLauncher); 353 icon = LinuxAppBundler.ICON_PNG.fetchFrom(addLauncher); 354 if (icon == null || !icon.exists()) { 355 fetchResource(iconTarget.getName(), 356 I18N.getString("resource.menu-icon"), 357 DEFAULT_ICON, 358 iconTarget, 359 VERBOSE.fetchFrom(params), 360 RESOURCE_DIR.fetchFrom(params)); 361 } else { 362 fetchResource(iconTarget.getName(), 363 I18N.getString("resource.menu-icon"), 364 icon, 365 iconTarget, 366 VERBOSE.fetchFrom(params), 367 RESOURCE_DIR.fetchFrom(params)); 368 } 369 370 // post copying of desktop icon 371 installScripts.append("xdg-desktop-menu install --novendor "); 372 installScripts.append(LINUX_INSTALL_DIR.fetchFrom(params)); 373 installScripts.append("/"); 374 installScripts.append(data.get("APPLICATION_FS_NAME")); 375 installScripts.append("/bin/"); 376 installScripts.append(addLauncherData.get( 377 "APPLICATION_LAUNCHER_FILENAME")); 378 installScripts.append(".desktop\n"); 379 380 // preun cleanup of desktop icon 381 removeScripts.append("xdg-desktop-menu uninstall --novendor "); 382 removeScripts.append(LINUX_INSTALL_DIR.fetchFrom(params)); 383 removeScripts.append("/"); 384 removeScripts.append(data.get("APPLICATION_FS_NAME")); 385 removeScripts.append("/bin/"); 386 removeScripts.append(addLauncherData.get( 387 "APPLICATION_LAUNCHER_FILENAME")); 388 removeScripts.append(".desktop\n"); 389 390 } 391 data.put("ADD_LAUNCHERS_INSTALL", installScripts.toString()); 392 data.put("ADD_LAUNCHERS_REMOVE", removeScripts.toString()); 393 394 StringBuilder cdsScript = new StringBuilder(); 395 396 data.put("APP_CDS_CACHE", cdsScript.toString()); 397 398 List<Map<String, ? super Object>> associations = 399 FILE_ASSOCIATIONS.fetchFrom(params); 400 data.put("FILE_ASSOCIATION_INSTALL", ""); 401 data.put("FILE_ASSOCIATION_REMOVE", ""); 402 data.put("DESKTOP_MIMES", ""); 403 if (associations != null) { 404 String mimeInfoFile = XDG_FILE_PREFIX.fetchFrom(params) 405 + "-MimeInfo.xml"; 406 StringBuilder mimeInfo = new StringBuilder( 407 "<?xml version=\"1.0\"?>\n<mime-info xmlns=" 408 +"'http://www.freedesktop.org/standards/shared-mime-info'>\n"); 409 StringBuilder registrations = new StringBuilder(); 410 StringBuilder deregistrations = new StringBuilder(); 411 StringBuilder desktopMimes = new StringBuilder("MimeType="); 412 boolean addedEntry = false; 413 414 for (Map<String, ? super Object> assoc : associations) { 415 // <mime-type type="application/x-vnd.awesome"> 416 // <comment>Awesome document</comment> 417 // <glob pattern="*.awesome"/> 418 // <glob pattern="*.awe"/> 419 // </mime-type> 420 421 if (assoc == null) { 422 continue; 423 } 424 425 String description = FA_DESCRIPTION.fetchFrom(assoc); 426 File faIcon = FA_ICON.fetchFrom(assoc); 427 List<String> extensions = FA_EXTENSIONS.fetchFrom(assoc); 428 if (extensions == null) { 429 Log.verbose(I18N.getString( 430 "message.creating-association-with-null-extension")); 431 } 432 433 List<String> mimes = FA_CONTENT_TYPE.fetchFrom(assoc); 434 if (mimes == null || mimes.isEmpty()) { 435 continue; 436 } 437 String thisMime = mimes.get(0); 438 String dashMime = thisMime.replace('/', '-'); 439 440 mimeInfo.append(" <mime-type type='") 441 .append(thisMime) 442 .append("'>\n"); 443 if (description != null && !description.isEmpty()) { 444 mimeInfo.append(" <comment>") 445 .append(description) 446 .append("</comment>\n"); 447 } 448 449 if (extensions != null) { 450 for (String ext : extensions) { 451 mimeInfo.append(" <glob pattern='*.") 452 .append(ext) 453 .append("'/>\n"); 454 } 455 } 456 457 mimeInfo.append(" </mime-type>\n"); 458 if (!addedEntry) { 459 registrations.append("xdg-mime install ") 460 .append(LINUX_INSTALL_DIR.fetchFrom(params)) 461 .append("/") 462 .append(data.get("APPLICATION_FS_NAME")) 463 .append("/bin/") 464 .append(mimeInfoFile) 465 .append("\n"); 466 467 deregistrations.append("xdg-mime uninstall ") 468 .append(LINUX_INSTALL_DIR.fetchFrom(params)) 469 .append("/") 470 .append(data.get("APPLICATION_FS_NAME")) 471 .append("/bin/") 472 .append(mimeInfoFile) 473 .append("\n"); 474 addedEntry = true; 475 } else { 476 desktopMimes.append(";"); 477 } 478 desktopMimes.append(thisMime); 479 480 if (faIcon != null && faIcon.exists()) { 481 int size = getSquareSizeOfImage(faIcon); 482 483 if (size > 0) { 484 File target = new File(binDir, 485 APP_NAME.fetchFrom(params) 486 + "_fa_" + faIcon.getName()); 487 IOUtils.copyFile(faIcon, target); 488 489 // xdg-icon-resource install --context mimetypes 490 // --size 64 awesomeapp_fa_1.png 491 // application-x.vnd-awesome 492 registrations.append( 493 "xdg-icon-resource install " 494 + "--context mimetypes --size ") 495 .append(size) 496 .append(" ") 497 .append(LINUX_INSTALL_DIR.fetchFrom(params)) 498 .append("/") 499 .append(data.get("APPLICATION_FS_NAME")) 500 .append("/") 501 .append(target.getName()) 502 .append(" ") 503 .append(dashMime) 504 .append("\n"); 505 506 // xdg-icon-resource uninstall --context mimetypes 507 // --size 64 awesomeapp_fa_1.png 508 // application-x.vnd-awesome 509 deregistrations.append( 510 "xdg-icon-resource uninstall " 511 + "--context mimetypes --size ") 512 .append(size) 513 .append(" ") 514 .append(LINUX_INSTALL_DIR.fetchFrom(params)) 515 .append("/") 516 .append(data.get("APPLICATION_FS_NAME")) 517 .append("/") 518 .append(target.getName()) 519 .append(" ") 520 .append(dashMime) 521 .append("\n"); 522 } 523 } 524 } 525 mimeInfo.append("</mime-info>"); 526 527 if (addedEntry) { 528 try (Writer w = Files.newBufferedWriter( 529 new File(binDir, mimeInfoFile).toPath())) { 530 w.write(mimeInfo.toString()); 531 } 532 data.put("FILE_ASSOCIATION_INSTALL", registrations.toString()); 533 data.put("FILE_ASSOCIATION_REMOVE", deregistrations.toString()); 534 data.put("DESKTOP_MIMES", desktopMimes.toString()); 535 } 536 } 537 538 if (!StandardBundlerParam.isRuntimeInstaller(params)) { 539 // prepare desktop shortcut 540 if (SHORTCUT_HINT.fetchFrom(params)) { 541 try (Writer w = Files.newBufferedWriter( 542 getConfig_DesktopShortcutFile(binDir, params).toPath())) { 543 String content = preprocessTextResource( 544 getConfig_DesktopShortcutFile(binDir, 545 params).getName(), 546 I18N.getString("resource.menu-shortcut-descriptor"), 547 DEFAULT_DESKTOP_FILE_TEMPLATE, data, 548 VERBOSE.fetchFrom(params), 549 RESOURCE_DIR.fetchFrom(params)); 550 w.write(content); 551 } 552 } 553 } 554 555 // prepare spec file 556 try (Writer w = Files.newBufferedWriter( 557 getConfig_SpecFile(params).toPath())) { 558 String content = preprocessTextResource( 559 getConfig_SpecFile(params).getName(), 560 I18N.getString("resource.rpm-spec-file"), 561 DEFAULT_SPEC_TEMPLATE, data, 562 VERBOSE.fetchFrom(params), 563 RESOURCE_DIR.fetchFrom(params)); 564 w.write(content); 565 } 566 567 return true; 568 } 569 570 private Map<String, String> createReplacementData( 571 Map<String, ? super Object> params) throws IOException { 572 Map<String, String> data = new HashMap<>(); 573 String launcher = LinuxAppImageBuilder.getLauncherRelativePath(params); 574 575 data.put("APPLICATION_NAME", APP_NAME.fetchFrom(params)); 576 data.put("APPLICATION_FS_NAME", APP_NAME.fetchFrom(params)); 577 data.put("APPLICATION_PACKAGE", BUNDLE_NAME.fetchFrom(params)); 578 data.put("APPLICATION_VENDOR", VENDOR.fetchFrom(params)); 579 data.put("APPLICATION_VERSION", VERSION.fetchFrom(params)); 580 data.put("APPLICATION_RELEASE", RELEASE.fetchFrom(params)); 581 data.put("APPLICATION_LAUNCHER_FILENAME", launcher); 582 data.put("INSTALLATION_DIRECTORY", LINUX_INSTALL_DIR.fetchFrom(params)); 583 data.put("XDG_PREFIX", XDG_FILE_PREFIX.fetchFrom(params)); 584 data.put("DEPLOY_BUNDLE_CATEGORY", MENU_GROUP.fetchFrom(params)); 585 data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params)); 586 data.put("APPLICATION_SUMMARY", APP_NAME.fetchFrom(params)); 587 data.put("APPLICATION_LICENSE_TYPE", LICENSE_TYPE.fetchFrom(params)); 588 589 String licenseFile = LICENSE_FILE.fetchFrom(params); 590 if (licenseFile == null) { 591 licenseFile = ""; 592 } 593 data.put("APPLICATION_LICENSE_FILE", licenseFile); 594 595 String group = GROUP.fetchFrom(params); 596 if (group == null) { 597 group = ""; 598 } 599 data.put("APPLICATION_GROUP", group); 600 601 String deps = LINUX_PACKAGE_DEPENDENCIES.fetchFrom(params); 602 data.put("PACKAGE_DEPENDENCIES", 603 deps.isEmpty() ? "" : "Requires: " + deps); 604 data.put("RUNTIME_INSTALLER", "" + 605 StandardBundlerParam.isRuntimeInstaller(params)); 606 return data; 607 } 608 609 private File getConfig_DesktopShortcutFile(File rootDir, 610 Map<String, ? super Object> params) { 611 return new File(rootDir, APP_NAME.fetchFrom(params) + ".desktop"); 612 } 613 614 private File getConfig_IconFile(File rootDir, 615 Map<String, ? super Object> params) { 616 return new File(rootDir, APP_NAME.fetchFrom(params) + ".png"); 617 } 618 619 private File getConfig_SpecFile(Map<String, ? super Object> params) { 620 return new File(RPM_IMAGE_DIR.fetchFrom(params), 621 APP_NAME.fetchFrom(params) + ".spec"); 622 } 623 624 private File buildRPM(Map<String, ? super Object> params, 625 File outdir) throws IOException { 626 Log.verbose(MessageFormat.format(I18N.getString( 627 "message.outputting-bundle-location"), 628 outdir.getAbsolutePath())); 629 630 File broot = new File(TEMP_ROOT.fetchFrom(params), "rmpbuildroot"); 631 632 outdir.mkdirs(); 633 634 //run rpmbuild 635 ProcessBuilder pb = new ProcessBuilder( 636 TOOL_RPMBUILD, 637 "-bb", getConfig_SpecFile(params).getAbsolutePath(), 638 "--define", "%_sourcedir " 639 + RPM_IMAGE_DIR.fetchFrom(params).getAbsolutePath(), 640 // save result to output dir 641 "--define", "%_rpmdir " + outdir.getAbsolutePath(), 642 // do not use other system directories to build as current user 643 "--define", "%_topdir " + broot.getAbsolutePath() 644 ); 645 pb = pb.directory(RPM_IMAGE_DIR.fetchFrom(params)); 646 IOUtils.exec(pb); 647 648 Log.verbose(MessageFormat.format( 649 I18N.getString("message.output-bundle-location"), 650 outdir.getAbsolutePath())); 651 652 // presume the result is the ".rpm" file with the newest modified time 653 // not the best solution, but it is the most reliable 654 File result = null; 655 long lastModified = 0; 656 File[] list = outdir.listFiles(); 657 if (list != null) { 658 for (File f : list) { 659 if (f.getName().endsWith(".rpm") && 660 f.lastModified() > lastModified) { 661 result = f; 662 lastModified = f.lastModified(); 663 } 664 } 665 } 666 667 return result; 668 } 669 670 @Override 671 public String getName() { 672 return I18N.getString("rpm.bundler.name"); 673 } 674 675 @Override 676 public String getID() { 677 return "rpm"; 678 } 679 680 @Override 681 public String getBundleType() { 682 return "INSTALLER"; 683 } 684 685 @Override 686 public File execute(Map<String, ? super Object> params, 687 File outputParentDir) throws PackagerException { 688 return bundle(params, outputParentDir); 689 } 690 691 @Override 692 public boolean supported(boolean runtimeInstaller) { 693 return isSupported(); 694 } 695 696 public static boolean isSupported() { 697 if (Platform.getPlatform() == Platform.LINUX) { 698 if (testTool(TOOL_RPMBUILD, TOOL_RPMBUILD_MIN_VERSION)) { 699 return true; 700 } 701 } 702 return false; 703 } 704 705 public int getSquareSizeOfImage(File f) { 706 try { 707 BufferedImage bi = ImageIO.read(f); 708 if (bi.getWidth() == bi.getHeight()) { 709 return bi.getWidth(); 710 } else { 711 return 0; 712 } 713 } catch (Exception e) { 714 e.printStackTrace(); 715 return 0; 716 } 717 } 718 719 @Override 720 public boolean isDefault() { 721 return !LinuxDebBundler.isDebian(); 722 } 723 724 } | 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 java.io.*; 29 import java.nio.file.Files; 30 import java.nio.file.Path; 31 import java.text.MessageFormat; 32 import java.util.*; 33 import java.util.regex.Matcher; 34 import java.util.regex.Pattern; 35 36 import static jdk.jpackage.internal.StandardBundlerParam.*; 37 import static jdk.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR; 38 39 /** 40 * There are two command line options to configure license information for RPM 41 * packaging: --linux-rpm-license-type and --license-file. Value of 42 * --linux-rpm-license-type command line option configures "License:" section 43 * of RPM spec. Value of --license-file command line option specifies a license 44 * file to be added to the package. License file is a sort of documentation file 45 * but it will be installed even if user selects an option to install the 46 * package without documentation. --linux-rpm-license-type is the primary option 47 * to set license information. --license-file makes little sense in case of RPM 48 * packaging. 49 */ 50 public class LinuxRpmBundler extends LinuxPackageBundler { 51 52 // Fedora rules for package naming are used here 53 // https://fedoraproject.org/wiki/Packaging:NamingGuidelines?rd=Packaging/NamingGuidelines 54 // 55 // all Fedora packages must be named using only the following ASCII 56 // characters. These characters are displayed here: 57 // 58 // abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._+ 59 // 60 private static final Pattern RPM_PACKAGE_NAME_PATTERN = 61 Pattern.compile("[a-z\\d\\+\\-\\.\\_]+", Pattern.CASE_INSENSITIVE); 62 63 public static final BundlerParamInfo<String> PACKAGE_NAME = 64 new StandardBundlerParam<> ( 65 Arguments.CLIOptions.LINUX_BUNDLE_NAME.getId(), 66 String.class, 67 params -> { 68 String nm = APP_NAME.fetchFrom(params); 69 if (nm == null) return null; 70 71 // make sure to lower case and spaces become dashes 72 nm = nm.toLowerCase().replaceAll("[ ]", "-"); 73 74 return nm; 75 }, 76 (s, p) -> { 77 if (!RPM_PACKAGE_NAME_PATTERN.matcher(s).matches()) { 78 String msgKey = "error.invalid-value-for-package-name"; 79 throw new IllegalArgumentException( 80 new ConfigException(MessageFormat.format( 81 I18N.getString(msgKey), s), 82 I18N.getString(msgKey + ".advice"))); 83 } 84 85 return s; 86 } 87 ); 88 89 public static final BundlerParamInfo<String> LICENSE_TYPE = 90 new StandardBundlerParam<>( 91 Arguments.CLIOptions.LINUX_RPM_LICENSE_TYPE.getId(), 92 String.class, 93 params -> I18N.getString("param.license-type.default"), 94 (s, p) -> s 95 ); 96 97 public static final BundlerParamInfo<String> GROUP = 98 new StandardBundlerParam<>( 99 Arguments.CLIOptions.LINUX_CATEGORY.getId(), 100 String.class, 101 params -> null, 102 (s, p) -> s); 103 104 private final static String DEFAULT_SPEC_TEMPLATE = "template.spec"; 105 106 public final static String TOOL_RPMBUILD = "rpmbuild"; 107 public final static double TOOL_RPMBUILD_MIN_VERSION = 4.0d; 108 109 public static boolean testTool(String toolName, double minVersion) { 110 try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); 111 PrintStream ps = new PrintStream(baos)) { 112 ProcessBuilder pb = new ProcessBuilder(toolName, "--version"); 113 IOUtils.exec(pb, false, ps); 114 //not interested in the above's output 115 String content = new String(baos.toByteArray()); 116 Pattern pattern = Pattern.compile(" (\\d+\\.\\d+)"); 117 Matcher matcher = pattern.matcher(content); 118 119 if (matcher.find()) { 120 String v = matcher.group(1); 121 double version = Double.parseDouble(v); 122 return minVersion <= version; 123 } else { 124 return false; 125 } 126 } catch (Exception e) { 127 Log.verbose(MessageFormat.format(I18N.getString( 128 "message.test-for-tool"), toolName, e.getMessage())); 129 return false; 130 } 131 } 132 133 public LinuxRpmBundler() { 134 super(PACKAGE_NAME); 135 } 136 137 @Override 138 public void doValidate(Map<String, ? super Object> params) 139 throws ConfigException { 140 if (params == null) throw new ConfigException( 141 I18N.getString("error.parameters-null"), 142 I18N.getString("error.parameters-null.advice")); 143 144 // validate presense of required tools 145 if (!testTool(TOOL_RPMBUILD, TOOL_RPMBUILD_MIN_VERSION)){ 146 throw new ConfigException( 147 MessageFormat.format( 148 I18N.getString("error.cannot-find-rpmbuild"), 149 TOOL_RPMBUILD_MIN_VERSION), 150 MessageFormat.format( 151 I18N.getString("error.cannot-find-rpmbuild.advice"), 152 TOOL_RPMBUILD_MIN_VERSION)); 153 } 154 } 155 156 @Override 157 protected File buildPackageBundle( 158 Map<String, String> replacementData, 159 Map<String, ? super Object> params, File outputParentDir) throws 160 PackagerException, IOException { 161 162 Path specFile = specFile(params); 163 164 // prepare spec file 165 Files.createDirectories(specFile.getParent()); 166 try (Writer w = Files.newBufferedWriter(specFile)) { 167 String content = preprocessTextResource( 168 specFile.getFileName().toString(), 169 I18N.getString("resource.rpm-spec-file"), 170 DEFAULT_SPEC_TEMPLATE, replacementData, 171 VERBOSE.fetchFrom(params), 172 RESOURCE_DIR.fetchFrom(params)); 173 w.write(content); 174 } 175 176 return buildRPM(params, outputParentDir); 177 } 178 179 @Override 180 protected Map<String, String> createReplacementData( 181 Map<String, ? super Object> params) throws IOException { 182 Map<String, String> data = new HashMap<>(); 183 184 data.put("APPLICATION_DIRECTORY", Path.of(LINUX_INSTALL_DIR.fetchFrom( 185 params), PACKAGE_NAME.fetchFrom(params)).toString()); 186 data.put("APPLICATION_SUMMARY", APP_NAME.fetchFrom(params)); 187 data.put("APPLICATION_LICENSE_TYPE", LICENSE_TYPE.fetchFrom(params)); 188 data.put("APPLICATION_LICENSE_FILE", Optional.ofNullable( 189 LICENSE_FILE.fetchFrom(params)).orElse("")); 190 data.put("APPLICATION_GROUP", Optional.ofNullable( 191 GROUP.fetchFrom(params)).orElse("")); 192 193 return data; 194 } 195 196 private Path specFile(Map<String, ? super Object> params) { 197 return TEMP_ROOT.fetchFrom(params).toPath().resolve(Path.of("SPECS", 198 PACKAGE_NAME.fetchFrom(params) + ".spec")); 199 } 200 201 private File buildRPM(Map<String, ? super Object> params, 202 File outdir) throws IOException { 203 Log.verbose(MessageFormat.format(I18N.getString( 204 "message.outputting-bundle-location"), 205 outdir.getAbsolutePath())); 206 207 PlatformPackage thePackage = createMetaPackage(params); 208 209 //run rpmbuild 210 ProcessBuilder pb = new ProcessBuilder( 211 TOOL_RPMBUILD, 212 "-bb", specFile(params).toAbsolutePath().toString(), 213 "--define", String.format("%%_sourcedir %s", thePackage.sourceRoot()), 214 // save result to output dir 215 "--define", String.format("%%_rpmdir %s", outdir.getAbsolutePath()), 216 // do not use other system directories to build as current user 217 "--define", String.format("%%_topdir %s", 218 TEMP_ROOT.fetchFrom(params).toPath().toAbsolutePath()) 219 ); 220 IOUtils.exec(pb); 221 222 Log.verbose(MessageFormat.format( 223 I18N.getString("message.output-bundle-location"), 224 outdir.getAbsolutePath())); 225 226 // presume the result is the ".rpm" file with the newest modified time 227 // not the best solution, but it is the most reliable 228 File result = null; 229 long lastModified = 0; 230 File[] list = outdir.listFiles(); 231 if (list != null) { 232 for (File f : list) { 233 if (f.getName().endsWith(".rpm") && 234 f.lastModified() > lastModified) { 235 result = f; 236 lastModified = f.lastModified(); 237 } 238 } 239 } 240 241 return result; 242 } 243 244 @Override 245 public String getName() { 246 return I18N.getString("rpm.bundler.name"); 247 } 248 249 @Override 250 public String getID() { 251 return "rpm"; 252 } 253 254 @Override 255 public boolean supported(boolean runtimeInstaller) { 256 if (Platform.getPlatform() == Platform.LINUX) { 257 if (testTool(TOOL_RPMBUILD, TOOL_RPMBUILD_MIN_VERSION)) { 258 return true; 259 } 260 } 261 return false; 262 } 263 264 @Override 265 public boolean isDefault() { 266 return !LinuxDebBundler.isDebian(); 267 } 268 269 } |