/* * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package jdk.jpackage.internal; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.*; import java.nio.file.Files; import java.text.MessageFormat; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import static jdk.jpackage.internal.StandardBundlerParam.*; import static jdk.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR; import static jdk.jpackage.internal.LinuxAppBundler.LINUX_PACKAGE_DEPENDENCIES; /** * There are two command line options to configure license information for RPM * packaging: --linux-rpm-license-type and --license-file. Value of * --linux-rpm-license-type command line option configures "License:" section * of RPM spec. Value of --license-file command line option specifies a license * file to be added to the package. License file is a sort of documentation file * but it will be installed even if user selects an option to install the * package without documentation. --linux-rpm-license-type is the primary option * to set license information. --license-file makes little sense in case of RPM * packaging. */ public class LinuxRpmBundler extends AbstractBundler { private static final ResourceBundle I18N = ResourceBundle.getBundle( "jdk.jpackage.internal.resources.LinuxResources"); public static final BundlerParamInfo APP_BUNDLER = new StandardBundlerParam<>( "linux.app.bundler", LinuxAppBundler.class, params -> new LinuxAppBundler(), null); public static final BundlerParamInfo RPM_IMAGE_DIR = new StandardBundlerParam<>( "linux.rpm.imageDir", File.class, params -> { File imagesRoot = IMAGES_ROOT.fetchFrom(params); if (!imagesRoot.exists()) imagesRoot.mkdirs(); return new File(imagesRoot, "linux-rpm.image"); }, (s, p) -> new File(s)); // Fedora rules for package naming are used here // https://fedoraproject.org/wiki/Packaging:NamingGuidelines?rd=Packaging/NamingGuidelines // // all Fedora packages must be named using only the following ASCII // characters. These characters are displayed here: // // abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._+ // private static final Pattern RPM_BUNDLE_NAME_PATTERN = Pattern.compile("[a-z\\d\\+\\-\\.\\_]+", Pattern.CASE_INSENSITIVE); public static final BundlerParamInfo BUNDLE_NAME = new StandardBundlerParam<> ( Arguments.CLIOptions.LINUX_BUNDLE_NAME.getId(), String.class, params -> { String nm = APP_NAME.fetchFrom(params); if (nm == null) return null; // make sure to lower case and spaces become dashes nm = nm.toLowerCase().replaceAll("[ ]", "-"); return nm; }, (s, p) -> { if (!RPM_BUNDLE_NAME_PATTERN.matcher(s).matches()) { String msgKey = "error.invalid-value-for-package-name"; throw new IllegalArgumentException( new ConfigException(MessageFormat.format( I18N.getString(msgKey), s), I18N.getString(msgKey + ".advice"))); } return s; } ); public static final BundlerParamInfo MENU_GROUP = new StandardBundlerParam<>( Arguments.CLIOptions.LINUX_MENU_GROUP.getId(), String.class, params -> I18N.getString("param.menu-group.default"), (s, p) -> s ); public static final BundlerParamInfo LICENSE_TYPE = new StandardBundlerParam<>( Arguments.CLIOptions.LINUX_RPM_LICENSE_TYPE.getId(), String.class, params -> I18N.getString("param.license-type.default"), (s, p) -> s ); public static final BundlerParamInfo GROUP = new StandardBundlerParam<>( Arguments.CLIOptions.LINUX_CATEGORY.getId(), String.class, params -> null, (s, p) -> s); public static final BundlerParamInfo XDG_FILE_PREFIX = new StandardBundlerParam<> ( "linux.xdg-prefix", String.class, params -> { try { String vendor; if (params.containsKey(VENDOR.getID())) { vendor = VENDOR.fetchFrom(params); } else { vendor = "jpackage"; } String appName = APP_NAME.fetchFrom(params); return (vendor + "-" + appName).replaceAll("\\s", ""); } catch (Exception e) { Log.verbose(e); } return "unknown-MimeInfo.xml"; }, (s, p) -> s); private final static String DEFAULT_ICON = "javalogo_white_32.png"; private final static String DEFAULT_SPEC_TEMPLATE = "template.spec"; private final static String DEFAULT_DESKTOP_FILE_TEMPLATE = "template.desktop"; public final static String TOOL_RPMBUILD = "rpmbuild"; public final static double TOOL_RPMBUILD_MIN_VERSION = 4.0d; public static boolean testTool(String toolName, double minVersion) { try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); PrintStream ps = new PrintStream(baos)) { ProcessBuilder pb = new ProcessBuilder(toolName, "--version"); IOUtils.exec(pb, false, ps); //not interested in the above's output String content = new String(baos.toByteArray()); Pattern pattern = Pattern.compile(" (\\d+\\.\\d+)"); Matcher matcher = pattern.matcher(content); if (matcher.find()) { String v = matcher.group(1); double version = Double.parseDouble(v); return minVersion <= version; } else { return false; } } catch (Exception e) { Log.verbose(MessageFormat.format(I18N.getString( "message.test-for-tool"), toolName, e.getMessage())); return false; } } @Override public boolean validate(Map params) throws ConfigException { try { if (params == null) throw new ConfigException( I18N.getString("error.parameters-null"), I18N.getString("error.parameters-null.advice")); // run basic validation to ensure requirements are met // we are not interested in return code, only possible exception APP_BUNDLER.fetchFrom(params).validate(params); // validate presense of required tools if (!testTool(TOOL_RPMBUILD, TOOL_RPMBUILD_MIN_VERSION)){ throw new ConfigException( MessageFormat.format( I18N.getString("error.cannot-find-rpmbuild"), TOOL_RPMBUILD_MIN_VERSION), MessageFormat.format( I18N.getString("error.cannot-find-rpmbuild.advice"), TOOL_RPMBUILD_MIN_VERSION)); } // only one mime type per association, at least one file extension List> associations = FILE_ASSOCIATIONS.fetchFrom(params); if (associations != null) { for (int i = 0; i < associations.size(); i++) { Map assoc = associations.get(i); List mimes = FA_CONTENT_TYPE.fetchFrom(assoc); if (mimes == null || mimes.isEmpty()) { String msgKey = "error.no-content-types-for-file-association"; throw new ConfigException( MessageFormat.format(I18N.getString(msgKey), i), I18N.getString(msgKey + ".advice")); } else if (mimes.size() > 1) { String msgKey = "error.no-content-types-for-file-association"; throw new ConfigException( MessageFormat.format(I18N.getString(msgKey), i), I18N.getString(msgKey + ".advice")); } } } // bundle name has some restrictions // the string converter will throw an exception if invalid BUNDLE_NAME.getStringConverter().apply( BUNDLE_NAME.fetchFrom(params), params); return true; } catch (RuntimeException re) { if (re.getCause() instanceof ConfigException) { throw (ConfigException) re.getCause(); } else { throw new ConfigException(re); } } } private boolean prepareProto(Map params) throws PackagerException, IOException { File appImage = StandardBundlerParam.getPredefinedAppImage(params); File appDir = null; // we either have an application image or need to build one if (appImage != null) { appDir = new File(RPM_IMAGE_DIR.fetchFrom(params), APP_NAME.fetchFrom(params)); // copy everything from appImage dir into appDir/name IOUtils.copyRecursive(appImage.toPath(), appDir.toPath()); } else { appDir = APP_BUNDLER.fetchFrom(params).doBundle(params, RPM_IMAGE_DIR.fetchFrom(params), true); } return appDir != null; } public File bundle(Map params, File outdir) throws PackagerException { IOUtils.writableOutputDir(outdir.toPath()); File imageDir = RPM_IMAGE_DIR.fetchFrom(params); try { imageDir.mkdirs(); if (prepareProto(params) && prepareProjectConfig(params)) { return buildRPM(params, outdir); } return null; } catch (IOException ex) { Log.verbose(ex); throw new PackagerException(ex); } } private boolean prepareProjectConfig(Map params) throws IOException { Map data = createReplacementData(params); File rootDir = LinuxAppBundler.getRootDir(RPM_IMAGE_DIR.fetchFrom(params), params); File binDir = new File(rootDir, "bin"); // prepare installer icon File iconTarget = getConfig_IconFile(binDir, params); File icon = LinuxAppBundler.ICON_PNG.fetchFrom(params); if (!StandardBundlerParam.isRuntimeInstaller(params)) { if (icon == null || !icon.exists()) { fetchResource(iconTarget.getName(), I18N.getString("resource.menu-icon"), DEFAULT_ICON, iconTarget, VERBOSE.fetchFrom(params), RESOURCE_DIR.fetchFrom(params)); } else { fetchResource(iconTarget.getName(), I18N.getString("resource.menu-icon"), icon, iconTarget, VERBOSE.fetchFrom(params), RESOURCE_DIR.fetchFrom(params)); } } StringBuilder installScripts = new StringBuilder(); StringBuilder removeScripts = new StringBuilder(); for (Map addLauncher : ADD_LAUNCHERS.fetchFrom(params)) { Map addLauncherData = createReplacementData(addLauncher); addLauncherData.put("APPLICATION_FS_NAME", data.get("APPLICATION_FS_NAME")); addLauncherData.put("DESKTOP_MIMES", ""); // prepare desktop shortcut try (Writer w = Files.newBufferedWriter( getConfig_DesktopShortcutFile(binDir, addLauncher).toPath())) { String content = preprocessTextResource( getConfig_DesktopShortcutFile(binDir, addLauncher).getName(), I18N.getString("resource.menu-shortcut-descriptor"), DEFAULT_DESKTOP_FILE_TEMPLATE, addLauncherData, VERBOSE.fetchFrom(params), RESOURCE_DIR.fetchFrom(params)); w.write(content); } // prepare installer icon iconTarget = getConfig_IconFile(binDir, addLauncher); icon = LinuxAppBundler.ICON_PNG.fetchFrom(addLauncher); if (icon == null || !icon.exists()) { fetchResource(iconTarget.getName(), I18N.getString("resource.menu-icon"), DEFAULT_ICON, iconTarget, VERBOSE.fetchFrom(params), RESOURCE_DIR.fetchFrom(params)); } else { fetchResource(iconTarget.getName(), I18N.getString("resource.menu-icon"), icon, iconTarget, VERBOSE.fetchFrom(params), RESOURCE_DIR.fetchFrom(params)); } // post copying of desktop icon installScripts.append("xdg-desktop-menu install --novendor "); installScripts.append(LINUX_INSTALL_DIR.fetchFrom(params)); installScripts.append("/"); installScripts.append(data.get("APPLICATION_FS_NAME")); installScripts.append("/bin/"); installScripts.append(addLauncherData.get( "APPLICATION_LAUNCHER_FILENAME")); installScripts.append(".desktop\n"); // preun cleanup of desktop icon removeScripts.append("xdg-desktop-menu uninstall --novendor "); removeScripts.append(LINUX_INSTALL_DIR.fetchFrom(params)); removeScripts.append("/"); removeScripts.append(data.get("APPLICATION_FS_NAME")); removeScripts.append("/bin/"); removeScripts.append(addLauncherData.get( "APPLICATION_LAUNCHER_FILENAME")); removeScripts.append(".desktop\n"); } data.put("ADD_LAUNCHERS_INSTALL", installScripts.toString()); data.put("ADD_LAUNCHERS_REMOVE", removeScripts.toString()); StringBuilder cdsScript = new StringBuilder(); data.put("APP_CDS_CACHE", cdsScript.toString()); List> associations = FILE_ASSOCIATIONS.fetchFrom(params); data.put("FILE_ASSOCIATION_INSTALL", ""); data.put("FILE_ASSOCIATION_REMOVE", ""); data.put("DESKTOP_MIMES", ""); if (associations != null) { String mimeInfoFile = XDG_FILE_PREFIX.fetchFrom(params) + "-MimeInfo.xml"; StringBuilder mimeInfo = new StringBuilder( "\n\n"); StringBuilder registrations = new StringBuilder(); StringBuilder deregistrations = new StringBuilder(); StringBuilder desktopMimes = new StringBuilder("MimeType="); boolean addedEntry = false; for (Map assoc : associations) { // // Awesome document // // // if (assoc == null) { continue; } String description = FA_DESCRIPTION.fetchFrom(assoc); File faIcon = FA_ICON.fetchFrom(assoc); List extensions = FA_EXTENSIONS.fetchFrom(assoc); if (extensions == null) { Log.verbose(I18N.getString( "message.creating-association-with-null-extension")); } List mimes = FA_CONTENT_TYPE.fetchFrom(assoc); if (mimes == null || mimes.isEmpty()) { continue; } String thisMime = mimes.get(0); String dashMime = thisMime.replace('/', '-'); mimeInfo.append(" \n"); if (description != null && !description.isEmpty()) { mimeInfo.append(" ") .append(description) .append("\n"); } if (extensions != null) { for (String ext : extensions) { mimeInfo.append(" \n"); } } mimeInfo.append(" \n"); if (!addedEntry) { registrations.append("xdg-mime install ") .append(LINUX_INSTALL_DIR.fetchFrom(params)) .append("/") .append(data.get("APPLICATION_FS_NAME")) .append("/bin/") .append(mimeInfoFile) .append("\n"); deregistrations.append("xdg-mime uninstall ") .append(LINUX_INSTALL_DIR.fetchFrom(params)) .append("/") .append(data.get("APPLICATION_FS_NAME")) .append("/bin/") .append(mimeInfoFile) .append("\n"); addedEntry = true; } else { desktopMimes.append(";"); } desktopMimes.append(thisMime); if (faIcon != null && faIcon.exists()) { int size = getSquareSizeOfImage(faIcon); if (size > 0) { File target = new File(binDir, APP_NAME.fetchFrom(params) + "_fa_" + faIcon.getName()); IOUtils.copyFile(faIcon, target); // xdg-icon-resource install --context mimetypes // --size 64 awesomeapp_fa_1.png // application-x.vnd-awesome registrations.append( "xdg-icon-resource install " + "--context mimetypes --size ") .append(size) .append(" ") .append(LINUX_INSTALL_DIR.fetchFrom(params)) .append("/") .append(data.get("APPLICATION_FS_NAME")) .append("/") .append(target.getName()) .append(" ") .append(dashMime) .append("\n"); // xdg-icon-resource uninstall --context mimetypes // --size 64 awesomeapp_fa_1.png // application-x.vnd-awesome deregistrations.append( "xdg-icon-resource uninstall " + "--context mimetypes --size ") .append(size) .append(" ") .append(LINUX_INSTALL_DIR.fetchFrom(params)) .append("/") .append(data.get("APPLICATION_FS_NAME")) .append("/") .append(target.getName()) .append(" ") .append(dashMime) .append("\n"); } } } mimeInfo.append(""); if (addedEntry) { try (Writer w = Files.newBufferedWriter( new File(binDir, mimeInfoFile).toPath())) { w.write(mimeInfo.toString()); } data.put("FILE_ASSOCIATION_INSTALL", registrations.toString()); data.put("FILE_ASSOCIATION_REMOVE", deregistrations.toString()); data.put("DESKTOP_MIMES", desktopMimes.toString()); } } if (!StandardBundlerParam.isRuntimeInstaller(params)) { //prepare desktop shortcut try (Writer w = Files.newBufferedWriter( getConfig_DesktopShortcutFile(binDir, params).toPath())) { String content = preprocessTextResource( getConfig_DesktopShortcutFile(binDir, params).getName(), I18N.getString("resource.menu-shortcut-descriptor"), DEFAULT_DESKTOP_FILE_TEMPLATE, data, VERBOSE.fetchFrom(params), RESOURCE_DIR.fetchFrom(params)); w.write(content); } } // prepare spec file try (Writer w = Files.newBufferedWriter( getConfig_SpecFile(params).toPath())) { String content = preprocessTextResource( getConfig_SpecFile(params).getName(), I18N.getString("resource.rpm-spec-file"), DEFAULT_SPEC_TEMPLATE, data, VERBOSE.fetchFrom(params), RESOURCE_DIR.fetchFrom(params)); w.write(content); } return true; } private Map createReplacementData( Map params) throws IOException { Map data = new HashMap<>(); String launcher = LinuxAppImageBuilder.getLauncherRelativePath(params); data.put("APPLICATION_NAME", APP_NAME.fetchFrom(params)); data.put("APPLICATION_FS_NAME", APP_NAME.fetchFrom(params)); data.put("APPLICATION_PACKAGE", BUNDLE_NAME.fetchFrom(params)); data.put("APPLICATION_VENDOR", VENDOR.fetchFrom(params)); data.put("APPLICATION_VERSION", VERSION.fetchFrom(params)); data.put("APPLICATION_RELEASE", RELEASE.fetchFrom(params)); data.put("APPLICATION_LAUNCHER_FILENAME", launcher); data.put("INSTALLATION_DIRECTORY", LINUX_INSTALL_DIR.fetchFrom(params)); data.put("XDG_PREFIX", XDG_FILE_PREFIX.fetchFrom(params)); data.put("DEPLOY_BUNDLE_CATEGORY", MENU_GROUP.fetchFrom(params)); data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params)); data.put("APPLICATION_SUMMARY", APP_NAME.fetchFrom(params)); data.put("APPLICATION_LICENSE_TYPE", LICENSE_TYPE.fetchFrom(params)); String licenseFile = LICENSE_FILE.fetchFrom(params); if (licenseFile == null) { licenseFile = ""; } data.put("APPLICATION_LICENSE_FILE", licenseFile); String group = GROUP.fetchFrom(params); if (group == null) { group = ""; } data.put("APPLICATION_GROUP", group); String deps = LINUX_PACKAGE_DEPENDENCIES.fetchFrom(params); data.put("PACKAGE_DEPENDENCIES", deps.isEmpty() ? "" : "Requires: " + deps); data.put("RUNTIME_INSTALLER", "" + StandardBundlerParam.isRuntimeInstaller(params)); return data; } private File getConfig_DesktopShortcutFile(File rootDir, Map params) { return new File(rootDir, APP_NAME.fetchFrom(params) + ".desktop"); } private File getConfig_IconFile(File rootDir, Map params) { return new File(rootDir, APP_NAME.fetchFrom(params) + ".png"); } private File getConfig_SpecFile(Map params) { return new File(RPM_IMAGE_DIR.fetchFrom(params), APP_NAME.fetchFrom(params) + ".spec"); } private File buildRPM(Map params, File outdir) throws IOException { Log.verbose(MessageFormat.format(I18N.getString( "message.outputting-bundle-location"), outdir.getAbsolutePath())); File broot = new File(TEMP_ROOT.fetchFrom(params), "rmpbuildroot"); outdir.mkdirs(); //run rpmbuild ProcessBuilder pb = new ProcessBuilder( TOOL_RPMBUILD, "-bb", getConfig_SpecFile(params).getAbsolutePath(), "--define", "%_sourcedir " + RPM_IMAGE_DIR.fetchFrom(params).getAbsolutePath(), // save result to output dir "--define", "%_rpmdir " + outdir.getAbsolutePath(), // do not use other system directories to build as current user "--define", "%_topdir " + broot.getAbsolutePath() ); pb = pb.directory(RPM_IMAGE_DIR.fetchFrom(params)); IOUtils.exec(pb); Log.verbose(MessageFormat.format( I18N.getString("message.output-bundle-location"), outdir.getAbsolutePath())); // presume the result is the ".rpm" file with the newest modified time // not the best solution, but it is the most reliable File result = null; long lastModified = 0; File[] list = outdir.listFiles(); if (list != null) { for (File f : list) { if (f.getName().endsWith(".rpm") && f.lastModified() > lastModified) { result = f; lastModified = f.lastModified(); } } } return result; } @Override public String getName() { return I18N.getString("rpm.bundler.name"); } @Override public String getID() { return "rpm"; } @Override public String getBundleType() { return "INSTALLER"; } @Override public File execute(Map params, File outputParentDir) throws PackagerException { return bundle(params, outputParentDir); } @Override public boolean supported(boolean runtimeInstaller) { return isSupported(); } public static boolean isSupported() { if (Platform.getPlatform() == Platform.LINUX) { if (testTool(TOOL_RPMBUILD, TOOL_RPMBUILD_MIN_VERSION)) { return true; } } return false; } public int getSquareSizeOfImage(File f) { try { BufferedImage bi = ImageIO.read(f); if (bi.getWidth() == bi.getHeight()) { return bi.getWidth(); } else { return 0; } } catch (Exception e) { e.printStackTrace(); return 0; } } }