1 /*
   2  * Copyright (c) 2012, 2020, 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.File;
  29 import java.io.IOException;
  30 import java.nio.file.FileVisitResult;
  31 import java.nio.file.Files;
  32 import java.nio.file.Path;
  33 import java.nio.file.SimpleFileVisitor;
  34 import java.nio.file.attribute.BasicFileAttributes;
  35 
  36 import java.nio.file.attribute.PosixFilePermission;
  37 import java.nio.file.attribute.PosixFilePermissions;
  38 import java.text.MessageFormat;
  39 import java.util.ArrayList;
  40 import java.util.HashMap;
  41 import java.util.HashSet;
  42 import java.util.List;
  43 import java.util.Map;
  44 import java.util.Set;
  45 import java.util.regex.Matcher;
  46 import java.util.regex.Pattern;
  47 import java.util.stream.Collectors;
  48 import java.util.stream.Stream;
  49 import static jdk.incubator.jpackage.internal.OverridableResource.createResource;
  50 import static jdk.incubator.jpackage.internal.StandardBundlerParam.APP_NAME;
  51 import static jdk.incubator.jpackage.internal.StandardBundlerParam.VERSION;
  52 import static jdk.incubator.jpackage.internal.StandardBundlerParam.RELEASE;
  53 import static jdk.incubator.jpackage.internal.StandardBundlerParam.VENDOR;
  54 import static jdk.incubator.jpackage.internal.StandardBundlerParam.LICENSE_FILE;
  55 import static jdk.incubator.jpackage.internal.StandardBundlerParam.COPYRIGHT;
  56 
  57 public class LinuxDebBundler extends LinuxPackageBundler {
  58 
  59     // Debian rules for package naming are used here
  60     // https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Source
  61     //
  62     // Package names must consist only of lower case letters (a-z),
  63     // digits (0-9), plus (+) and minus (-) signs, and periods (.).
  64     // They must be at least two characters long and
  65     // must start with an alphanumeric character.
  66     //
  67     private static final Pattern DEB_PACKAGE_NAME_PATTERN =
  68             Pattern.compile("^[a-z][a-z\\d\\+\\-\\.]+");
  69 
  70     private static final BundlerParamInfo<String> PACKAGE_NAME =
  71             new StandardBundlerParam<> (
  72             Arguments.CLIOptions.LINUX_BUNDLE_NAME.getId(),
  73             String.class,
  74             params -> {
  75                 String nm = APP_NAME.fetchFrom(params);
  76 
  77                 if (nm == null) return null;
  78 
  79                 // make sure to lower case and spaces/underscores become dashes
  80                 nm = nm.toLowerCase().replaceAll("[ _]", "-");
  81                 return nm;
  82             },
  83             (s, p) -> {
  84                 if (!DEB_PACKAGE_NAME_PATTERN.matcher(s).matches()) {
  85                     throw new IllegalArgumentException(new ConfigException(
  86                             MessageFormat.format(I18N.getString(
  87                             "error.invalid-value-for-package-name"), s),
  88                             I18N.getString(
  89                             "error.invalid-value-for-package-name.advice")));
  90                 }
  91 
  92                 return s;
  93             });
  94 
  95     private final static String TOOL_DPKG_DEB = "dpkg-deb";
  96     private final static String TOOL_DPKG = "dpkg";
  97     private final static String TOOL_FAKEROOT = "fakeroot";
  98 
  99     private final static String DEB_ARCH;
 100     static {
 101         String debArch;
 102         try {
 103             debArch = Executor.of(TOOL_DPKG, "--print-architecture").saveOutput(
 104                     true).executeExpectSuccess().getOutput().get(0);
 105         } catch (IOException ex) {
 106             debArch = null;
 107         }
 108         DEB_ARCH = debArch;
 109     }
 110 
 111     private static final BundlerParamInfo<String> FULL_PACKAGE_NAME =
 112             new StandardBundlerParam<>(
 113                     "linux.deb.fullPackageName", String.class, params -> {
 114                         return PACKAGE_NAME.fetchFrom(params)
 115                             + "_" + VERSION.fetchFrom(params)
 116                             + "-" + RELEASE.fetchFrom(params)
 117                             + "_" + DEB_ARCH;
 118                     }, (s, p) -> s);
 119 
 120     private static final BundlerParamInfo<String> EMAIL =
 121             new StandardBundlerParam<> (
 122             Arguments.CLIOptions.LINUX_DEB_MAINTAINER.getId(),
 123             String.class,
 124             params -> "Unknown",
 125             (s, p) -> s);
 126 
 127     private static final BundlerParamInfo<String> MAINTAINER =
 128             new StandardBundlerParam<> (
 129             Arguments.CLIOptions.LINUX_DEB_MAINTAINER.getId() + ".internal",
 130             String.class,
 131             params -> VENDOR.fetchFrom(params) + " <"
 132                     + EMAIL.fetchFrom(params) + ">",
 133             (s, p) -> s);
 134 
 135     private static final BundlerParamInfo<String> SECTION =
 136             new StandardBundlerParam<>(
 137             Arguments.CLIOptions.LINUX_CATEGORY.getId(),
 138             String.class,
 139             params -> "misc",
 140             (s, p) -> s);
 141 
 142     private static final BundlerParamInfo<String> LICENSE_TEXT =
 143             new StandardBundlerParam<> (
 144             "linux.deb.licenseText",
 145             String.class,
 146             params -> {
 147                 try {
 148                     String licenseFile = LICENSE_FILE.fetchFrom(params);
 149                     if (licenseFile != null) {
 150                         return Files.readString(Path.of(licenseFile));
 151                     }
 152                 } catch (IOException e) {
 153                     Log.verbose(e);
 154                 }
 155                 return "Unknown";
 156             },
 157             (s, p) -> s);
 158 
 159     public LinuxDebBundler() {
 160         super(PACKAGE_NAME);
 161     }
 162 
 163     @Override
 164     public void doValidate(Map<String, ? super Object> params)
 165             throws ConfigException {
 166 
 167         // Show warning if license file is missing
 168         if (LICENSE_FILE.fetchFrom(params) == null) {
 169             Log.verbose(I18N.getString("message.debs-like-licenses"));
 170         }
 171     }
 172 
 173     @Override
 174     protected List<ToolValidator> getToolValidators(
 175             Map<String, ? super Object> params) {
 176         return Stream.of(TOOL_DPKG_DEB, TOOL_DPKG, TOOL_FAKEROOT).map(
 177                 ToolValidator::new).collect(Collectors.toList());
 178     }
 179 
 180     @Override
 181     protected File buildPackageBundle(
 182             Map<String, String> replacementData,
 183             Map<String, ? super Object> params, File outputParentDir) throws
 184             PackagerException, IOException {
 185 
 186         prepareProjectConfig(replacementData, params);
 187         adjustPermissionsRecursive(createMetaPackage(params).sourceRoot().toFile());
 188         return buildDeb(params, outputParentDir);
 189     }
 190 
 191     private static final Pattern PACKAGE_NAME_REGEX = Pattern.compile("^(^\\S+):");
 192 
 193     @Override
 194     protected void initLibProvidersLookup(
 195             Map<String, ? super Object> params,
 196             LibProvidersLookup libProvidersLookup) {
 197 
 198         //
 199         // `dpkg -S` command does glob pattern lookup. If not the absolute path
 200         // to the file is specified it might return mltiple package names.
 201         // Even for full paths multiple package names can be returned as
 202         // it is OK for multiple packages to provide the same file. `/opt`
 203         // directory is such an example. So we have to deal with multiple
 204         // packages per file situation.
 205         //
 206         // E.g.: `dpkg -S libc.so.6` command reports three packages:
 207         // libc6-x32: /libx32/libc.so.6
 208         // libc6:amd64: /lib/x86_64-linux-gnu/libc.so.6
 209         // libc6-i386: /lib32/libc.so.6
 210         // `:amd64` is architecture suffix and can (should) be dropped.
 211         // Still need to decide what package to choose from three.
 212         // libc6-x32 and libc6-i386 both depend on libc6:
 213         // $ dpkg -s libc6-x32
 214         // Package: libc6-x32
 215         // Status: install ok installed
 216         // Priority: optional
 217         // Section: libs
 218         // Installed-Size: 10840
 219         // Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
 220         // Architecture: amd64
 221         // Source: glibc
 222         // Version: 2.23-0ubuntu10
 223         // Depends: libc6 (= 2.23-0ubuntu10)
 224         //
 225         // We can dive into tracking dependencies, but this would be overly
 226         // complicated.
 227         //
 228         // For simplicity lets consider the following rules:
 229         // 1. If there is one item in `dpkg -S` output, accept it.
 230         // 2. If there are multiple items in `dpkg -S` output and there is at
 231         //  least one item with the default arch suffix (DEB_ARCH),
 232         //  accept only these items.
 233         // 3. If there are multiple items in `dpkg -S` output and there are
 234         //  no with the default arch suffix (DEB_ARCH), accept all items.
 235         // So lets use this heuristics: don't accept packages for whom
 236         //  `dpkg -p` command fails.
 237         // 4. Arch suffix should be stripped from accepted package names.
 238         //
 239 
 240         libProvidersLookup.setPackageLookup(file -> {
 241             Set<String> archPackages = new HashSet<>();
 242             Set<String> otherPackages = new HashSet<>();
 243 
 244             Executor.of(TOOL_DPKG, "-S", file.toString())
 245                     .saveOutput(true).executeExpectSuccess()
 246                     .getOutput().forEach(line -> {
 247                         Matcher matcher = PACKAGE_NAME_REGEX.matcher(line);
 248                         if (matcher.find()) {
 249                             String name = matcher.group(1);
 250                             if (name.endsWith(":" + DEB_ARCH)) {
 251                                 // Strip arch suffix
 252                                 name = name.substring(0,
 253                                         name.length() - (DEB_ARCH.length() + 1));
 254                                 archPackages.add(name);
 255                             } else {
 256                                 otherPackages.add(name);
 257                             }
 258                         }
 259                     });
 260 
 261             if (!archPackages.isEmpty()) {
 262                 return archPackages.stream();
 263             }
 264             return otherPackages.stream();
 265         });
 266     }
 267 
 268     @Override
 269     protected List<ConfigException> verifyOutputBundle(
 270             Map<String, ? super Object> params, Path packageBundle) {
 271         List<ConfigException> errors = new ArrayList<>();
 272 
 273         String controlFileName = "control";
 274 
 275         List<PackageProperty> properties = List.of(
 276                 new PackageProperty("Package", PACKAGE_NAME.fetchFrom(params),
 277                         "APPLICATION_PACKAGE", controlFileName),
 278                 new PackageProperty("Version", String.format("%s-%s",
 279                         VERSION.fetchFrom(params), RELEASE.fetchFrom(params)),
 280                         "APPLICATION_VERSION-APPLICATION_RELEASE",
 281                         controlFileName),
 282                 new PackageProperty("Architecture", DEB_ARCH, "APPLICATION_ARCH",
 283                         controlFileName));
 284 
 285         List<String> cmdline = new ArrayList<>(List.of(TOOL_DPKG_DEB, "-f",
 286                 packageBundle.toString()));
 287         properties.forEach(property -> cmdline.add(property.name));
 288         try {
 289             Map<String, String> actualValues = Executor.of(cmdline.toArray(String[]::new))
 290                     .saveOutput(true)
 291                     .executeExpectSuccess()
 292                     .getOutput().stream()
 293                             .map(line -> line.split(":\\s+", 2))
 294                             .collect(Collectors.toMap(
 295                                     components -> components[0],
 296                                     components -> components[1]));
 297             properties.forEach(property -> errors.add(property.verifyValue(
 298                     actualValues.get(property.name))));
 299         } catch (IOException ex) {
 300             // Ignore error as it is not critical. Just report it.
 301             Log.verbose(ex);
 302         }
 303 
 304         return errors;
 305     }
 306 
 307     /*
 308      * set permissions with a string like "rwxr-xr-x"
 309      *
 310      * This cannot be directly backport to 22u which is built with 1.6
 311      */
 312     private void setPermissions(File file, String permissions) {
 313         Set<PosixFilePermission> filePermissions =
 314                 PosixFilePermissions.fromString(permissions);
 315         try {
 316             if (file.exists()) {
 317                 Files.setPosixFilePermissions(file.toPath(), filePermissions);
 318             }
 319         } catch (IOException ex) {
 320             Log.error(ex.getMessage());
 321             Log.verbose(ex);
 322         }
 323 
 324     }
 325 
 326     public static boolean isDebian() {
 327         // we are just going to run "dpkg -s coreutils" and assume Debian
 328         // or deritive if no error is returned.
 329         try {
 330             Executor.of(TOOL_DPKG, "-s", "coreutils").executeExpectSuccess();
 331             return true;
 332         } catch (IOException e) {
 333             // just fall thru
 334         }
 335         return false;
 336     }
 337 
 338     private void adjustPermissionsRecursive(File dir) throws IOException {
 339         Files.walkFileTree(dir.toPath(), new SimpleFileVisitor<Path>() {
 340             @Override
 341             public FileVisitResult visitFile(Path file,
 342                     BasicFileAttributes attrs)
 343                     throws IOException {
 344                 if (file.endsWith(".so") || !Files.isExecutable(file)) {
 345                     setPermissions(file.toFile(), "rw-r--r--");
 346                 } else if (Files.isExecutable(file)) {
 347                     setPermissions(file.toFile(), "rwxr-xr-x");
 348                 }
 349                 return FileVisitResult.CONTINUE;
 350             }
 351 
 352             @Override
 353             public FileVisitResult postVisitDirectory(Path dir, IOException e)
 354                     throws IOException {
 355                 if (e == null) {
 356                     setPermissions(dir.toFile(), "rwxr-xr-x");
 357                     return FileVisitResult.CONTINUE;
 358                 } else {
 359                     // directory iteration failed
 360                     throw e;
 361                 }
 362             }
 363         });
 364     }
 365 
 366     private class DebianFile {
 367 
 368         DebianFile(Path dstFilePath, String comment) {
 369             this.dstFilePath = dstFilePath;
 370             this.comment = comment;
 371         }
 372 
 373         DebianFile setExecutable() {
 374             permissions = "rwxr-xr-x";
 375             return this;
 376         }
 377 
 378         void create(Map<String, String> data, Map<String, ? super Object> params)
 379                 throws IOException {
 380             createResource("template." + dstFilePath.getFileName().toString(),
 381                     params)
 382                     .setCategory(I18N.getString(comment))
 383                     .setSubstitutionData(data)
 384                     .saveToFile(dstFilePath);
 385             if (permissions != null) {
 386                 setPermissions(dstFilePath.toFile(), permissions);
 387             }
 388         }
 389 
 390         private final Path dstFilePath;
 391         private final String comment;
 392         private String permissions;
 393     }
 394 
 395     private void prepareProjectConfig(Map<String, String> data,
 396             Map<String, ? super Object> params) throws IOException {
 397 
 398         Path configDir = createMetaPackage(params).sourceRoot().resolve("DEBIAN");
 399         List<DebianFile> debianFiles = new ArrayList<>();
 400         debianFiles.add(new DebianFile(
 401                 configDir.resolve("control"),
 402                 "resource.deb-control-file"));
 403         debianFiles.add(new DebianFile(
 404                 configDir.resolve("preinst"),
 405                 "resource.deb-preinstall-script").setExecutable());
 406         debianFiles.add(new DebianFile(
 407                 configDir.resolve("prerm"),
 408                 "resource.deb-prerm-script").setExecutable());
 409         debianFiles.add(new DebianFile(
 410                 configDir.resolve("postinst"),
 411                 "resource.deb-postinstall-script").setExecutable());
 412         debianFiles.add(new DebianFile(
 413                 configDir.resolve("postrm"),
 414                 "resource.deb-postrm-script").setExecutable());
 415 
 416         if (!StandardBundlerParam.isRuntimeInstaller(params)) {
 417             debianFiles.add(new DebianFile(
 418                     getConfig_CopyrightFile(params).toPath(),
 419                     "resource.copyright-file"));
 420         }
 421 
 422         for (DebianFile debianFile : debianFiles) {
 423             debianFile.create(data, params);
 424         }
 425     }
 426 
 427     @Override
 428     protected Map<String, String> createReplacementData(
 429             Map<String, ? super Object> params) throws IOException {
 430         Map<String, String> data = new HashMap<>();
 431 
 432         data.put("APPLICATION_MAINTAINER", MAINTAINER.fetchFrom(params));
 433         data.put("APPLICATION_SECTION", SECTION.fetchFrom(params));
 434         data.put("APPLICATION_COPYRIGHT", COPYRIGHT.fetchFrom(params));
 435         data.put("APPLICATION_LICENSE_TEXT", LICENSE_TEXT.fetchFrom(params));
 436         data.put("APPLICATION_ARCH", DEB_ARCH);
 437         data.put("APPLICATION_INSTALLED_SIZE", Long.toString(
 438                 createMetaPackage(params).sourceApplicationLayout().sizeInBytes() >> 10));
 439 
 440         return data;
 441     }
 442 
 443     private File getConfig_CopyrightFile(Map<String, ? super Object> params) {
 444         final String installDir = LINUX_INSTALL_DIR.fetchFrom(params);
 445         final String packageName = PACKAGE_NAME.fetchFrom(params);
 446 
 447         final Path installPath;
 448         if (isInstallDirInUsrTree(installDir) || installDir.startsWith("/usr/")) {
 449             installPath = Path.of("/usr/share/doc/", packageName, "copyright");
 450         } else {
 451             installPath = Path.of(installDir, packageName, "share/doc/copyright");
 452         }
 453 
 454         return createMetaPackage(params).sourceRoot().resolve(
 455                 Path.of("/").relativize(installPath)).toFile();
 456     }
 457 
 458     private File buildDeb(Map<String, ? super Object> params,
 459             File outdir) throws IOException {
 460         File outFile = new File(outdir,
 461                 FULL_PACKAGE_NAME.fetchFrom(params)+".deb");
 462         Log.verbose(MessageFormat.format(I18N.getString(
 463                 "message.outputting-to-location"), outFile.getAbsolutePath()));
 464 
 465         PlatformPackage thePackage = createMetaPackage(params);
 466 
 467         List<String> cmdline = new ArrayList<>();
 468         cmdline.addAll(List.of(TOOL_FAKEROOT, TOOL_DPKG_DEB));
 469         if (Log.isVerbose()) {
 470             cmdline.add("--verbose");
 471         }
 472         cmdline.addAll(List.of("-b", thePackage.sourceRoot().toString(),
 473                 outFile.getAbsolutePath()));
 474 
 475         // run dpkg
 476         Executor.of(cmdline.toArray(String[]::new)).executeExpectSuccess();
 477 
 478         Log.verbose(MessageFormat.format(I18N.getString(
 479                 "message.output-to-location"), outFile.getAbsolutePath()));
 480 
 481         return outFile;
 482     }
 483 
 484     @Override
 485     public String getName() {
 486         return I18N.getString("deb.bundler.name");
 487     }
 488 
 489     @Override
 490     public String getID() {
 491         return "deb";
 492     }
 493 
 494     @Override
 495     public boolean supported(boolean runtimeInstaller) {
 496         return Platform.isLinux() && (new ToolValidator(TOOL_DPKG_DEB).validate() == null);
 497     }
 498 
 499     @Override
 500     public boolean isDefault() {
 501         return isDebian();
 502     }
 503 }