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 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 }