src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxRpmBundler.java
branchJDK-8200758-branch
changeset 58994 b09ba68c6a19
parent 58890 6539ad1d90aa
equal deleted inserted replaced
58993:b5e1baa9d2c3 58994:b09ba68c6a19
       
     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.incubator.jpackage.internal;
       
    27 
       
    28 import java.io.*;
       
    29 import java.nio.file.Path;
       
    30 import java.text.MessageFormat;
       
    31 import java.util.*;
       
    32 import java.util.regex.Matcher;
       
    33 import java.util.regex.Pattern;
       
    34 import java.util.stream.Collectors;
       
    35 import java.util.stream.Stream;
       
    36 
       
    37 import static jdk.incubator.jpackage.internal.StandardBundlerParam.*;
       
    38 import static jdk.incubator.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR;
       
    39 import static jdk.incubator.jpackage.internal.OverridableResource.createResource;
       
    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 LinuxPackageBundler {
       
    53 
       
    54     // Fedora rules for package naming are used here
       
    55     // https://fedoraproject.org/wiki/Packaging:NamingGuidelines?rd=Packaging/NamingGuidelines
       
    56     //
       
    57     // all Fedora packages must be named using only the following ASCII
       
    58     // characters. These characters are displayed here:
       
    59     //
       
    60     // abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._+
       
    61     //
       
    62     private static final Pattern RPM_PACKAGE_NAME_PATTERN =
       
    63             Pattern.compile("[a-z\\d\\+\\-\\.\\_]+", Pattern.CASE_INSENSITIVE);
       
    64 
       
    65     public static final BundlerParamInfo<String> PACKAGE_NAME =
       
    66             new StandardBundlerParam<> (
       
    67             Arguments.CLIOptions.LINUX_BUNDLE_NAME.getId(),
       
    68             String.class,
       
    69             params -> {
       
    70                 String nm = APP_NAME.fetchFrom(params);
       
    71                 if (nm == null) return null;
       
    72 
       
    73                 // make sure to lower case and spaces become dashes
       
    74                 nm = nm.toLowerCase().replaceAll("[ ]", "-");
       
    75 
       
    76                 return nm;
       
    77             },
       
    78             (s, p) -> {
       
    79                 if (!RPM_PACKAGE_NAME_PATTERN.matcher(s).matches()) {
       
    80                     String msgKey = "error.invalid-value-for-package-name";
       
    81                     throw new IllegalArgumentException(
       
    82                             new ConfigException(MessageFormat.format(
       
    83                                     I18N.getString(msgKey), s),
       
    84                                     I18N.getString(msgKey + ".advice")));
       
    85                 }
       
    86 
       
    87                 return s;
       
    88             }
       
    89         );
       
    90 
       
    91     public static final BundlerParamInfo<String> LICENSE_TYPE =
       
    92         new StandardBundlerParam<>(
       
    93                 Arguments.CLIOptions.LINUX_RPM_LICENSE_TYPE.getId(),
       
    94                 String.class,
       
    95                 params -> I18N.getString("param.license-type.default"),
       
    96                 (s, p) -> s
       
    97         );
       
    98 
       
    99     public static final BundlerParamInfo<String> GROUP =
       
   100             new StandardBundlerParam<>(
       
   101             Arguments.CLIOptions.LINUX_CATEGORY.getId(),
       
   102             String.class,
       
   103             params -> null,
       
   104             (s, p) -> s);
       
   105 
       
   106     private final static String DEFAULT_SPEC_TEMPLATE = "template.spec";
       
   107 
       
   108     public final static String TOOL_RPM = "rpm";
       
   109     public final static String TOOL_RPMBUILD = "rpmbuild";
       
   110     public final static DottedVersion TOOL_RPMBUILD_MIN_VERSION = DottedVersion.lazy(
       
   111             "4.0");
       
   112 
       
   113     public LinuxRpmBundler() {
       
   114         super(PACKAGE_NAME);
       
   115     }
       
   116 
       
   117     @Override
       
   118     public void doValidate(Map<String, ? super Object> params)
       
   119             throws ConfigException {
       
   120     }
       
   121 
       
   122     private static ToolValidator createRpmbuildToolValidator() {
       
   123         Pattern pattern = Pattern.compile(" (\\d+\\.\\d+)");
       
   124         return new ToolValidator(TOOL_RPMBUILD).setMinimalVersion(
       
   125                 TOOL_RPMBUILD_MIN_VERSION).setVersionParser(lines -> {
       
   126                     String versionString = lines.limit(1).collect(
       
   127                             Collectors.toList()).get(0);
       
   128                     Matcher matcher = pattern.matcher(versionString);
       
   129                     if (matcher.find()) {
       
   130                         return matcher.group(1);
       
   131                     }
       
   132                     return null;
       
   133                 });
       
   134     }
       
   135 
       
   136     @Override
       
   137     protected List<ToolValidator> getToolValidators(
       
   138             Map<String, ? super Object> params) {
       
   139         return List.of(createRpmbuildToolValidator());
       
   140     }
       
   141 
       
   142     @Override
       
   143     protected File buildPackageBundle(
       
   144             Map<String, String> replacementData,
       
   145             Map<String, ? super Object> params, File outputParentDir) throws
       
   146             PackagerException, IOException {
       
   147 
       
   148         Path specFile = specFile(params);
       
   149 
       
   150         // prepare spec file
       
   151         createResource(DEFAULT_SPEC_TEMPLATE, params)
       
   152                 .setCategory(I18N.getString("resource.rpm-spec-file"))
       
   153                 .setSubstitutionData(replacementData)
       
   154                 .saveToFile(specFile);
       
   155 
       
   156         return buildRPM(params, outputParentDir.toPath()).toFile();
       
   157     }
       
   158 
       
   159     @Override
       
   160     protected Map<String, String> createReplacementData(
       
   161             Map<String, ? super Object> params) throws IOException {
       
   162         Map<String, String> data = new HashMap<>();
       
   163 
       
   164         data.put("APPLICATION_DIRECTORY", Path.of(LINUX_INSTALL_DIR.fetchFrom(
       
   165                 params), PACKAGE_NAME.fetchFrom(params)).toString());
       
   166         data.put("APPLICATION_SUMMARY", APP_NAME.fetchFrom(params));
       
   167         data.put("APPLICATION_LICENSE_TYPE", LICENSE_TYPE.fetchFrom(params));
       
   168 
       
   169         String licenseFile = LICENSE_FILE.fetchFrom(params);
       
   170         if (licenseFile != null) {
       
   171             licenseFile = Path.of(licenseFile).toAbsolutePath().normalize().toString();
       
   172         }
       
   173         data.put("APPLICATION_LICENSE_FILE", licenseFile);
       
   174         data.put("APPLICATION_GROUP", GROUP.fetchFrom(params));
       
   175 
       
   176         return data;
       
   177     }
       
   178 
       
   179     @Override
       
   180     protected void initLibProvidersLookup(
       
   181             Map<String, ? super Object> params,
       
   182             LibProvidersLookup libProvidersLookup) {
       
   183         libProvidersLookup.setPackageLookup(file -> {
       
   184             return Executor.of(TOOL_RPM,
       
   185                 "-q", "--queryformat", "%{name}\\n",
       
   186                 "-q", "--whatprovides", file.toString())
       
   187                 .saveOutput(true).executeExpectSuccess().getOutput().stream();
       
   188         });
       
   189     }
       
   190 
       
   191     @Override
       
   192     protected List<ConfigException> verifyOutputBundle(
       
   193             Map<String, ? super Object> params, Path packageBundle) {
       
   194         List<ConfigException> errors = new ArrayList<>();
       
   195 
       
   196         String specFileName = specFile(params).getFileName().toString();
       
   197 
       
   198         try {
       
   199             List<PackageProperty> properties = List.of(
       
   200                     new PackageProperty("Name", PACKAGE_NAME.fetchFrom(params),
       
   201                             "APPLICATION_PACKAGE", specFileName),
       
   202                     new PackageProperty("Version", VERSION.fetchFrom(params),
       
   203                             "APPLICATION_VERSION", specFileName),
       
   204                     new PackageProperty("Release", RELEASE.fetchFrom(params),
       
   205                             "APPLICATION_RELEASE", specFileName),
       
   206                     new PackageProperty("Arch", rpmArch(), null, specFileName));
       
   207 
       
   208             List<String> actualValues = Executor.of(TOOL_RPM, "-qp", "--queryformat",
       
   209                     properties.stream().map(entry -> String.format("%%{%s}",
       
   210                     entry.name)).collect(Collectors.joining("\\n")),
       
   211                     packageBundle.toString()).saveOutput(true).executeExpectSuccess().getOutput();
       
   212 
       
   213             Iterator<String> actualValuesIt = actualValues.iterator();
       
   214             properties.forEach(property -> errors.add(property.verifyValue(
       
   215                     actualValuesIt.next())));
       
   216         } catch (IOException ex) {
       
   217             // Ignore error as it is not critical. Just report it.
       
   218             Log.verbose(ex);
       
   219         }
       
   220 
       
   221         return errors;
       
   222     }
       
   223 
       
   224     /**
       
   225      * Various ways to get rpm arch. Needed to address JDK-8233143. rpmbuild is
       
   226      * mandatory for rpm packaging, try it first. rpm is optional and may not be
       
   227      * available, use as the last resort.
       
   228      */
       
   229     private enum RpmArchReader {
       
   230         Rpmbuild(TOOL_RPMBUILD, "--eval=%{_target_cpu}"),
       
   231         Rpm(TOOL_RPM, "--eval=%{_target_cpu}");
       
   232 
       
   233         RpmArchReader(String... cmdline) {
       
   234             this.cmdline = cmdline;
       
   235         }
       
   236 
       
   237         String getRpmArch() throws IOException {
       
   238             Executor exec = Executor.of(cmdline).saveOutput(true);
       
   239             if (this == values()[values().length - 1]) {
       
   240                 exec.executeExpectSuccess();
       
   241             } else if (exec.execute() != 0) {
       
   242                 return null;
       
   243             }
       
   244 
       
   245             return exec.getOutput().get(0);
       
   246         }
       
   247 
       
   248         private final String[] cmdline;
       
   249     }
       
   250 
       
   251     private String rpmArch() throws IOException {
       
   252         if (rpmArch == null) {
       
   253             for (var rpmArchReader : RpmArchReader.values()) {
       
   254                 rpmArch = rpmArchReader.getRpmArch();
       
   255                 if (rpmArch != null) {
       
   256                     break;
       
   257                 }
       
   258             }
       
   259         }
       
   260         return rpmArch;
       
   261     }
       
   262 
       
   263     private Path specFile(Map<String, ? super Object> params) {
       
   264         return TEMP_ROOT.fetchFrom(params).toPath().resolve(Path.of("SPECS",
       
   265                 PACKAGE_NAME.fetchFrom(params) + ".spec"));
       
   266     }
       
   267 
       
   268     private Path buildRPM(Map<String, ? super Object> params,
       
   269             Path outdir) throws IOException {
       
   270 
       
   271         Path rpmFile = outdir.toAbsolutePath().resolve(String.format(
       
   272                 "%s-%s-%s.%s.rpm", PACKAGE_NAME.fetchFrom(params),
       
   273                 VERSION.fetchFrom(params), RELEASE.fetchFrom(params), rpmArch()));
       
   274 
       
   275         Log.verbose(MessageFormat.format(I18N.getString(
       
   276                 "message.outputting-bundle-location"),
       
   277                 rpmFile.getParent()));
       
   278 
       
   279         PlatformPackage thePackage = createMetaPackage(params);
       
   280 
       
   281         //run rpmbuild
       
   282         Executor.of(
       
   283                 TOOL_RPMBUILD,
       
   284                 "-bb", specFile(params).toAbsolutePath().toString(),
       
   285                 "--define", String.format("%%_sourcedir %s",
       
   286                         thePackage.sourceRoot()),
       
   287                 // save result to output dir
       
   288                 "--define", String.format("%%_rpmdir %s", rpmFile.getParent()),
       
   289                 // do not use other system directories to build as current user
       
   290                 "--define", String.format("%%_topdir %s",
       
   291                         TEMP_ROOT.fetchFrom(params).toPath().toAbsolutePath()),
       
   292                 "--define", String.format("%%_rpmfilename %s", rpmFile.getFileName())
       
   293         ).executeExpectSuccess();
       
   294 
       
   295         Log.verbose(MessageFormat.format(
       
   296                 I18N.getString("message.output-bundle-location"),
       
   297                 rpmFile.getParent()));
       
   298 
       
   299         return rpmFile;
       
   300     }
       
   301 
       
   302     @Override
       
   303     public String getName() {
       
   304         return I18N.getString("rpm.bundler.name");
       
   305     }
       
   306 
       
   307     @Override
       
   308     public String getID() {
       
   309         return "rpm";
       
   310     }
       
   311 
       
   312     @Override
       
   313     public boolean supported(boolean runtimeInstaller) {
       
   314         return Platform.isLinux() && (createRpmbuildToolValidator().validate() == null);
       
   315     }
       
   316 
       
   317     @Override
       
   318     public boolean isDefault() {
       
   319         return !LinuxDebBundler.isDebian();
       
   320     }
       
   321 
       
   322     private String rpmArch;
       
   323 }