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.charset.StandardCharsets;
  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.*;
  40 import java.util.regex.Pattern;
  41 import java.util.stream.Stream;
  42 import static jdk.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR;
  43 
  44 import static jdk.jpackage.internal.StandardBundlerParam.*;
  45 import static jdk.jpackage.internal.LinuxPackageBundler.I18N;
  46 
  47 public class LinuxDebBundler extends LinuxPackageBundler {
  48 
  49     // Debian rules for package naming are used here
  50     // https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Source
  51     //
  52     // Package names must consist only of lower case letters (a-z),
  53     // digits (0-9), plus (+) and minus (-) signs, and periods (.).
  54     // They must be at least two characters long and
  55     // must start with an alphanumeric character.
  56     //
  57     private static final Pattern DEB_PACKAGE_NAME_PATTERN =
  58             Pattern.compile("^[a-z][a-z\\d\\+\\-\\.]+");
  59 
  60     private static final BundlerParamInfo<String> PACKAGE_NAME =
  61             new StandardBundlerParam<> (
  62             Arguments.CLIOptions.LINUX_BUNDLE_NAME.getId(),
  63             String.class,
  64             params -> {
  65                 String nm = APP_NAME.fetchFrom(params);
  66 
  67                 if (nm == null) return null;
  68 
  69                 // make sure to lower case and spaces/underscores become dashes
  70                 nm = nm.toLowerCase().replaceAll("[ _]", "-");
  71                 return nm;
  72             },
  73             (s, p) -> {
  74                 if (!DEB_PACKAGE_NAME_PATTERN.matcher(s).matches()) {
  75                     throw new IllegalArgumentException(new ConfigException(
  76                             MessageFormat.format(I18N.getString(
  77                             "error.invalid-value-for-package-name"), s),
  78                             I18N.getString(
  79                             "error.invalid-value-for-package-name.advice")));
  80                 }
  81 
  82                 return s;
  83             });
  84 
  85     private static final BundlerParamInfo<String> FULL_PACKAGE_NAME =
  86             new StandardBundlerParam<>(
  87                     "linux.deb.fullPackageName", String.class, params -> {
  88                         try {
  89                             return PACKAGE_NAME.fetchFrom(params)
  90                             + "_" + VERSION.fetchFrom(params)
  91                             + "-" + RELEASE.fetchFrom(params)
  92                             + "_" + getDebArch();
  93                         } catch (IOException ex) {
  94                             Log.verbose(ex);
  95                             return null;
  96                         }
  97                     }, (s, p) -> s);
  98 
  99     private static final BundlerParamInfo<String> EMAIL =
 100             new StandardBundlerParam<> (
 101             Arguments.CLIOptions.LINUX_DEB_MAINTAINER.getId(),
 102             String.class,
 103             params -> "Unknown",
 104             (s, p) -> s);
 105 
 106     private static final BundlerParamInfo<String> MAINTAINER =
 107             new StandardBundlerParam<> (
 108             BundleParams.PARAM_MAINTAINER,
 109             String.class,
 110             params -> VENDOR.fetchFrom(params) + " <"
 111                     + EMAIL.fetchFrom(params) + ">",
 112             (s, p) -> s);
 113 
 114     private static final BundlerParamInfo<String> SECTION =
 115             new StandardBundlerParam<>(
 116             Arguments.CLIOptions.LINUX_CATEGORY.getId(),
 117             String.class,
 118             params -> "misc",
 119             (s, p) -> s);
 120 
 121     private static final BundlerParamInfo<String> LICENSE_TEXT =
 122             new StandardBundlerParam<> (
 123             "linux.deb.licenseText",
 124             String.class,
 125             params -> {
 126                 try {
 127                     String licenseFile = LICENSE_FILE.fetchFrom(params);
 128                     if (licenseFile != null) {
 129                         StringBuilder contentBuilder = new StringBuilder();
 130                         try (Stream<String> stream = Files.lines(Path.of(
 131                                 licenseFile), StandardCharsets.UTF_8)) {
 132                             stream.forEach(s -> contentBuilder.append(s).append(
 133                                     "\n"));
 134                         }
 135                         return contentBuilder.toString();
 136                     }
 137                 } catch (Exception e) {
 138                     Log.verbose(e);
 139                 }
 140                 return "Unknown";
 141             },
 142             (s, p) -> s);
 143 
 144     private static final BundlerParamInfo<String> COPYRIGHT_FILE =
 145             new StandardBundlerParam<>(
 146             Arguments.CLIOptions.LINUX_DEB_COPYRIGHT_FILE.getId(),
 147             String.class,
 148             params -> null,
 149             (s, p) -> s);
 150 
 151     private final static String TOOL_DPKG_DEB = "dpkg-deb";
 152     private final static String TOOL_DPKG = "dpkg";
 153 
 154     public static boolean testTool(String toolName, String minVersion) {
 155         try {
 156             ProcessBuilder pb = new ProcessBuilder(
 157                     toolName,
 158                     "--version");
 159             // not interested in the output
 160             IOUtils.exec(pb, true, null);
 161         } catch (Exception e) {
 162             Log.verbose(MessageFormat.format(I18N.getString(
 163                     "message.test-for-tool"), toolName, e.getMessage()));
 164             return false;
 165         }
 166         return true;
 167     }
 168 
 169     public LinuxDebBundler() {
 170         super(PACKAGE_NAME);
 171     }
 172 
 173     @Override
 174     public void doValidate(Map<String, ? super Object> params)
 175             throws ConfigException {
 176         // NOTE: Can we validate that the required tools are available
 177         // before we start?
 178         if (!testTool(TOOL_DPKG_DEB, "1")){
 179             throw new ConfigException(MessageFormat.format(
 180                     I18N.getString("error.tool-not-found"), TOOL_DPKG_DEB),
 181                     I18N.getString("error.tool-not-found.advice"));
 182         }
 183         if (!testTool(TOOL_DPKG, "1")){
 184             throw new ConfigException(MessageFormat.format(
 185                     I18N.getString("error.tool-not-found"), TOOL_DPKG),
 186                     I18N.getString("error.tool-not-found.advice"));
 187         }
 188 
 189 
 190         // Show warning is license file is missing
 191         String licenseFile = LICENSE_FILE.fetchFrom(params);
 192         if (licenseFile == null) {
 193             Log.verbose(I18N.getString("message.debs-like-licenses"));
 194         }
 195     }
 196 
 197     @Override
 198     protected File buildPackageBundle(
 199             Map<String, String> replacementData,
 200             Map<String, ? super Object> params, File outputParentDir) throws
 201             PackagerException, IOException {
 202 
 203         prepareProjectConfig(replacementData, params);
 204         adjustPermissionsRecursive(createMetaPackage(params).sourceRoot().toFile());
 205         return buildDeb(params, outputParentDir);
 206     }
 207 
 208     /*
 209      * set permissions with a string like "rwxr-xr-x"
 210      *
 211      * This cannot be directly backport to 22u which is built with 1.6
 212      */
 213     private void setPermissions(File file, String permissions) {
 214         Set<PosixFilePermission> filePermissions =
 215                 PosixFilePermissions.fromString(permissions);
 216         try {
 217             if (file.exists()) {
 218                 Files.setPosixFilePermissions(file.toPath(), filePermissions);
 219             }
 220         } catch (IOException ex) {
 221             Log.error(ex.getMessage());
 222             Log.verbose(ex);
 223         }
 224 
 225     }
 226 
 227     private static String getDebArch() throws IOException {
 228         try (var baos = new ByteArrayOutputStream();
 229                 var ps = new PrintStream(baos)) {
 230             var pb = new ProcessBuilder(TOOL_DPKG, "--print-architecture");
 231             IOUtils.exec(pb, false, ps);
 232             return baos.toString().split("\n", 2)[0];
 233         }
 234     }
 235 
 236     public static boolean isDebian() {
 237         // we are just going to run "dpkg -s coreutils" and assume Debian
 238         // or deritive if no error is returned.
 239         var pb = new ProcessBuilder(TOOL_DPKG, "-s", "coreutils");
 240         try {
 241             int ret = pb.start().waitFor();
 242             return (ret == 0);
 243         } catch (IOException | InterruptedException e) {
 244             // just fall thru
 245         }
 246         return false;
 247     }
 248 
 249     private void adjustPermissionsRecursive(File dir) throws IOException {
 250         Files.walkFileTree(dir.toPath(), new SimpleFileVisitor<Path>() {
 251             @Override
 252             public FileVisitResult visitFile(Path file,
 253                     BasicFileAttributes attrs)
 254                     throws IOException {
 255                 if (file.endsWith(".so") || !Files.isExecutable(file)) {
 256                     setPermissions(file.toFile(), "rw-r--r--");
 257                 } else if (Files.isExecutable(file)) {
 258                     setPermissions(file.toFile(), "rwxr-xr-x");
 259                 }
 260                 return FileVisitResult.CONTINUE;
 261             }
 262 
 263             @Override
 264             public FileVisitResult postVisitDirectory(Path dir, IOException e)
 265                     throws IOException {
 266                 if (e == null) {
 267                     setPermissions(dir.toFile(), "rwxr-xr-x");
 268                     return FileVisitResult.CONTINUE;
 269                 } else {
 270                     // directory iteration failed
 271                     throw e;
 272                 }
 273             }
 274         });
 275     }
 276 
 277     private class DebianFile {
 278 
 279         DebianFile(Path dstFilePath, String comment) {
 280             this.dstFilePath = dstFilePath;
 281             this.comment = comment;
 282         }
 283 
 284         DebianFile setExecutable() {
 285             permissions = "rwxr-xr-x";
 286             return this;
 287         }
 288 
 289         void create(Map<String, String> data, Map<String, ? super Object> params)
 290                 throws IOException {
 291             Files.createDirectories(dstFilePath.getParent());
 292             try (Writer w = Files.newBufferedWriter(dstFilePath)) {
 293                 String content = preprocessTextResource(
 294                         dstFilePath.getFileName().toString(),
 295                         I18N.getString(comment),
 296                         "template." + dstFilePath.getFileName().toString(),
 297                         data,
 298                         VERBOSE.fetchFrom(params),
 299                         RESOURCE_DIR.fetchFrom(params));
 300                 w.write(content);
 301             }
 302             if (permissions != null) {
 303                 setPermissions(dstFilePath.toFile(), permissions);
 304             }
 305         }
 306 
 307         private final Path dstFilePath;
 308         private final String comment;
 309         private String permissions;
 310     }
 311 
 312     private void prepareProjectConfig(Map<String, String> data,
 313             Map<String, ? super Object> params) throws IOException {
 314 
 315         Path configDir = createMetaPackage(params).sourceRoot().resolve("DEBIAN");
 316         List<DebianFile> debianFiles = new ArrayList<>();
 317         debianFiles.add(new DebianFile(
 318                 configDir.resolve("control"),
 319                 "resource.deb-control-file"));
 320         debianFiles.add(new DebianFile(
 321                 configDir.resolve("preinst"),
 322                 "resource.deb-preinstall-script").setExecutable());
 323         debianFiles.add(new DebianFile(
 324                 configDir.resolve("prerm"),
 325                 "resource.deb-prerm-script").setExecutable());
 326         debianFiles.add(new DebianFile(
 327                 configDir.resolve("postinst"),
 328                 "resource.deb-postinstall-script").setExecutable());
 329         debianFiles.add(new DebianFile(
 330                 configDir.resolve("postrm"),
 331                 "resource.deb-postrm-script").setExecutable());
 332 
 333         getConfig_CopyrightFile(params).getParentFile().mkdirs();
 334         String customCopyrightFile = COPYRIGHT_FILE.fetchFrom(params);
 335         if (customCopyrightFile != null) {
 336             IOUtils.copyFile(new File(customCopyrightFile),
 337                     getConfig_CopyrightFile(params));
 338         } else {
 339             debianFiles.add(new DebianFile(
 340                     getConfig_CopyrightFile(params).toPath(),
 341                     "resource.copyright-file"));
 342         }
 343 
 344         for (DebianFile debianFile : debianFiles) {
 345             debianFile.create(data, params);
 346         }
 347     }
 348 
 349     @Override
 350     protected Map<String, String> createReplacementData(
 351             Map<String, ? super Object> params) throws IOException {
 352         Map<String, String> data = new HashMap<>();
 353 
 354         data.put("APPLICATION_MAINTAINER", MAINTAINER.fetchFrom(params));
 355         data.put("APPLICATION_SECTION", SECTION.fetchFrom(params));
 356         data.put("APPLICATION_COPYRIGHT", COPYRIGHT.fetchFrom(params));
 357         data.put("APPLICATION_LICENSE_TEXT", LICENSE_TEXT.fetchFrom(params));
 358         data.put("APPLICATION_ARCH", getDebArch());
 359         data.put("APPLICATION_INSTALLED_SIZE", Long.toString(
 360                 createMetaPackage(params).sourceApplicationLayout().sizeInBytes() >> 10));
 361 
 362         return data;
 363     }
 364 
 365     private File getConfig_CopyrightFile(Map<String, ? super Object> params) {
 366         PlatformPackage thePackage = createMetaPackage(params);
 367         return thePackage.sourceRoot().resolve(Path.of(".",
 368                 LINUX_INSTALL_DIR.fetchFrom(params), PACKAGE_NAME.fetchFrom(
 369                 params), "share/doc/copyright")).toFile();
 370     }
 371 
 372     private File buildDeb(Map<String, ? super Object> params,
 373             File outdir) throws IOException {
 374         File outFile = new File(outdir,
 375                 FULL_PACKAGE_NAME.fetchFrom(params)+".deb");
 376         Log.verbose(MessageFormat.format(I18N.getString(
 377                 "message.outputting-to-location"), outFile.getAbsolutePath()));
 378 
 379         PlatformPackage thePackage = createMetaPackage(params);
 380 
 381         // run dpkg
 382         ProcessBuilder pb = new ProcessBuilder(
 383                 "fakeroot", TOOL_DPKG_DEB, "-b",
 384                 thePackage.sourceRoot().toString(),
 385                 outFile.getAbsolutePath());
 386         IOUtils.exec(pb);
 387 
 388         Log.verbose(MessageFormat.format(I18N.getString(
 389                 "message.output-to-location"), outFile.getAbsolutePath()));
 390 
 391         return outFile;
 392     }
 393 
 394     @Override
 395     public String getName() {
 396         return I18N.getString("deb.bundler.name");
 397     }
 398 
 399     @Override
 400     public String getID() {
 401         return "deb";
 402     }
 403 
 404     @Override
 405     public boolean supported(boolean runtimeInstaller) {
 406         if (Platform.getPlatform() == Platform.LINUX) {
 407             if (testTool(TOOL_DPKG_DEB, "1")) {
 408                 return true;
 409             }
 410         }
 411         return false;
 412     }
 413 
 414     @Override
 415     public boolean isDefault() {
 416         return isDebian();
 417     }
 418 
 419 }