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