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