--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.jpackage/linux/classes/jdk/incubator/jpackage/internal/LinuxDebBundler.java Fri Nov 08 14:53:03 2019 -0500
@@ -0,0 +1,487 @@
+/*
+ * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.jpackage.internal;
+
+import java.io.*;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.text.MessageFormat;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import static jdk.incubator.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR;
+import static jdk.incubator.jpackage.internal.OverridableResource.createResource;
+
+import static jdk.incubator.jpackage.internal.StandardBundlerParam.*;
+
+
+public class LinuxDebBundler extends LinuxPackageBundler {
+
+ // Debian rules for package naming are used here
+ // https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Source
+ //
+ // Package names must consist only of lower case letters (a-z),
+ // digits (0-9), plus (+) and minus (-) signs, and periods (.).
+ // They must be at least two characters long and
+ // must start with an alphanumeric character.
+ //
+ private static final Pattern DEB_PACKAGE_NAME_PATTERN =
+ Pattern.compile("^[a-z][a-z\\d\\+\\-\\.]+");
+
+ private static final BundlerParamInfo<String> PACKAGE_NAME =
+ new StandardBundlerParam<> (
+ Arguments.CLIOptions.LINUX_BUNDLE_NAME.getId(),
+ String.class,
+ params -> {
+ String nm = APP_NAME.fetchFrom(params);
+
+ if (nm == null) return null;
+
+ // make sure to lower case and spaces/underscores become dashes
+ nm = nm.toLowerCase().replaceAll("[ _]", "-");
+ return nm;
+ },
+ (s, p) -> {
+ if (!DEB_PACKAGE_NAME_PATTERN.matcher(s).matches()) {
+ throw new IllegalArgumentException(new ConfigException(
+ MessageFormat.format(I18N.getString(
+ "error.invalid-value-for-package-name"), s),
+ I18N.getString(
+ "error.invalid-value-for-package-name.advice")));
+ }
+
+ return s;
+ });
+
+ private final static String TOOL_DPKG_DEB = "dpkg-deb";
+ private final static String TOOL_DPKG = "dpkg";
+ private final static String TOOL_FAKEROOT = "fakeroot";
+
+ private final static String DEB_ARCH;
+ static {
+ String debArch;
+ try {
+ debArch = Executor.of(TOOL_DPKG, "--print-architecture").saveOutput(
+ true).executeExpectSuccess().getOutput().get(0);
+ } catch (IOException ex) {
+ debArch = null;
+ }
+ DEB_ARCH = debArch;
+ }
+
+ private static final BundlerParamInfo<String> FULL_PACKAGE_NAME =
+ new StandardBundlerParam<>(
+ "linux.deb.fullPackageName", String.class, params -> {
+ return PACKAGE_NAME.fetchFrom(params)
+ + "_" + VERSION.fetchFrom(params)
+ + "-" + RELEASE.fetchFrom(params)
+ + "_" + DEB_ARCH;
+ }, (s, p) -> s);
+
+ private static final BundlerParamInfo<String> EMAIL =
+ new StandardBundlerParam<> (
+ Arguments.CLIOptions.LINUX_DEB_MAINTAINER.getId(),
+ String.class,
+ params -> "Unknown",
+ (s, p) -> s);
+
+ private static final BundlerParamInfo<String> MAINTAINER =
+ new StandardBundlerParam<> (
+ BundleParams.PARAM_MAINTAINER,
+ String.class,
+ params -> VENDOR.fetchFrom(params) + " <"
+ + EMAIL.fetchFrom(params) + ">",
+ (s, p) -> s);
+
+ private static final BundlerParamInfo<String> SECTION =
+ new StandardBundlerParam<>(
+ Arguments.CLIOptions.LINUX_CATEGORY.getId(),
+ String.class,
+ params -> "misc",
+ (s, p) -> s);
+
+ private static final BundlerParamInfo<String> LICENSE_TEXT =
+ new StandardBundlerParam<> (
+ "linux.deb.licenseText",
+ String.class,
+ params -> {
+ try {
+ String licenseFile = LICENSE_FILE.fetchFrom(params);
+ if (licenseFile != null) {
+ return Files.readString(Path.of(licenseFile));
+ }
+ } catch (IOException e) {
+ Log.verbose(e);
+ }
+ return "Unknown";
+ },
+ (s, p) -> s);
+
+ public LinuxDebBundler() {
+ super(PACKAGE_NAME);
+ }
+
+ @Override
+ public void doValidate(Map<String, ? super Object> params)
+ throws ConfigException {
+
+ // Show warning if license file is missing
+ if (LICENSE_FILE.fetchFrom(params) == null) {
+ Log.verbose(I18N.getString("message.debs-like-licenses"));
+ }
+ }
+
+ @Override
+ protected List<ToolValidator> getToolValidators(
+ Map<String, ? super Object> params) {
+ return Stream.of(TOOL_DPKG_DEB, TOOL_DPKG, TOOL_FAKEROOT).map(
+ ToolValidator::new).collect(Collectors.toList());
+ }
+
+ @Override
+ protected File buildPackageBundle(
+ Map<String, String> replacementData,
+ Map<String, ? super Object> params, File outputParentDir) throws
+ PackagerException, IOException {
+
+ prepareProjectConfig(replacementData, params);
+ adjustPermissionsRecursive(createMetaPackage(params).sourceRoot().toFile());
+ return buildDeb(params, outputParentDir);
+ }
+
+ private static final Pattern PACKAGE_NAME_REGEX = Pattern.compile("^(^\\S+):");
+
+ @Override
+ protected void initLibProvidersLookup(
+ Map<String, ? super Object> params,
+ LibProvidersLookup libProvidersLookup) {
+
+ //
+ // `dpkg -S` command does glob pattern lookup. If not the absolute path
+ // to the file is specified it might return mltiple package names.
+ // Even for full paths multiple package names can be returned as
+ // it is OK for multiple packages to provide the same file. `/opt`
+ // directory is such an example. So we have to deal with multiple
+ // packages per file situation.
+ //
+ // E.g.: `dpkg -S libc.so.6` command reports three packages:
+ // libc6-x32: /libx32/libc.so.6
+ // libc6:amd64: /lib/x86_64-linux-gnu/libc.so.6
+ // libc6-i386: /lib32/libc.so.6
+ // `:amd64` is architecture suffix and can (should) be dropped.
+ // Still need to decide what package to choose from three.
+ // libc6-x32 and libc6-i386 both depend on libc6:
+ // $ dpkg -s libc6-x32
+ // Package: libc6-x32
+ // Status: install ok installed
+ // Priority: optional
+ // Section: libs
+ // Installed-Size: 10840
+ // Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
+ // Architecture: amd64
+ // Source: glibc
+ // Version: 2.23-0ubuntu10
+ // Depends: libc6 (= 2.23-0ubuntu10)
+ //
+ // We can dive into tracking dependencies, but this would be overly
+ // complicated.
+ //
+ // For simplicity lets consider the following rules:
+ // 1. If there is one item in `dpkg -S` output, accept it.
+ // 2. If there are multiple items in `dpkg -S` output and there is at
+ // least one item with the default arch suffix (DEB_ARCH),
+ // accept only these items.
+ // 3. If there are multiple items in `dpkg -S` output and there are
+ // no with the default arch suffix (DEB_ARCH), accept all items.
+ // So lets use this heuristics: don't accept packages for whom
+ // `dpkg -p` command fails.
+ // 4. Arch suffix should be stripped from accepted package names.
+ //
+
+ libProvidersLookup.setPackageLookup(file -> {
+ Set<String> archPackages = new HashSet<>();
+ Set<String> otherPackages = new HashSet<>();
+
+ Executor.of(TOOL_DPKG, "-S", file.toString())
+ .saveOutput(true).executeExpectSuccess()
+ .getOutput().forEach(line -> {
+ Matcher matcher = PACKAGE_NAME_REGEX.matcher(line);
+ if (matcher.find()) {
+ String name = matcher.group(1);
+ if (name.endsWith(":" + DEB_ARCH)) {
+ // Strip arch suffix
+ name = name.substring(0,
+ name.length() - (DEB_ARCH.length() + 1));
+ archPackages.add(name);
+ } else {
+ otherPackages.add(name);
+ }
+ }
+ });
+
+ if (!archPackages.isEmpty()) {
+ return archPackages.stream();
+ }
+ return otherPackages.stream();
+ });
+ }
+
+ @Override
+ protected List<ConfigException> verifyOutputBundle(
+ Map<String, ? super Object> params, Path packageBundle) {
+ List<ConfigException> errors = new ArrayList<>();
+
+ String controlFileName = "control";
+
+ List<PackageProperty> properties = List.of(
+ new PackageProperty("Package", PACKAGE_NAME.fetchFrom(params),
+ "APPLICATION_PACKAGE", controlFileName),
+ new PackageProperty("Version", String.format("%s-%s",
+ VERSION.fetchFrom(params), RELEASE.fetchFrom(params)),
+ "APPLICATION_VERSION-APPLICATION_RELEASE",
+ controlFileName),
+ new PackageProperty("Architecture", DEB_ARCH, "APPLICATION_ARCH",
+ controlFileName));
+
+ List<String> cmdline = new ArrayList<>(List.of(TOOL_DPKG_DEB, "-f",
+ packageBundle.toString()));
+ properties.forEach(property -> cmdline.add(property.name));
+ try {
+ Map<String, String> actualValues = Executor.of(cmdline.toArray(String[]::new))
+ .saveOutput(true)
+ .executeExpectSuccess()
+ .getOutput().stream()
+ .map(line -> line.split(":\\s+", 2))
+ .collect(Collectors.toMap(
+ components -> components[0],
+ components -> components[1]));
+ properties.forEach(property -> errors.add(property.verifyValue(
+ actualValues.get(property.name))));
+ } catch (IOException ex) {
+ // Ignore error as it is not critical. Just report it.
+ Log.verbose(ex);
+ }
+
+ return errors;
+ }
+
+ /*
+ * set permissions with a string like "rwxr-xr-x"
+ *
+ * This cannot be directly backport to 22u which is built with 1.6
+ */
+ private void setPermissions(File file, String permissions) {
+ Set<PosixFilePermission> filePermissions =
+ PosixFilePermissions.fromString(permissions);
+ try {
+ if (file.exists()) {
+ Files.setPosixFilePermissions(file.toPath(), filePermissions);
+ }
+ } catch (IOException ex) {
+ Log.error(ex.getMessage());
+ Log.verbose(ex);
+ }
+
+ }
+
+ public static boolean isDebian() {
+ // we are just going to run "dpkg -s coreutils" and assume Debian
+ // or deritive if no error is returned.
+ try {
+ Executor.of(TOOL_DPKG, "-s", "coreutils").executeExpectSuccess();
+ return true;
+ } catch (IOException e) {
+ // just fall thru
+ }
+ return false;
+ }
+
+ private void adjustPermissionsRecursive(File dir) throws IOException {
+ Files.walkFileTree(dir.toPath(), new SimpleFileVisitor<Path>() {
+ @Override
+ public FileVisitResult visitFile(Path file,
+ BasicFileAttributes attrs)
+ throws IOException {
+ if (file.endsWith(".so") || !Files.isExecutable(file)) {
+ setPermissions(file.toFile(), "rw-r--r--");
+ } else if (Files.isExecutable(file)) {
+ setPermissions(file.toFile(), "rwxr-xr-x");
+ }
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException e)
+ throws IOException {
+ if (e == null) {
+ setPermissions(dir.toFile(), "rwxr-xr-x");
+ return FileVisitResult.CONTINUE;
+ } else {
+ // directory iteration failed
+ throw e;
+ }
+ }
+ });
+ }
+
+ private class DebianFile {
+
+ DebianFile(Path dstFilePath, String comment) {
+ this.dstFilePath = dstFilePath;
+ this.comment = comment;
+ }
+
+ DebianFile setExecutable() {
+ permissions = "rwxr-xr-x";
+ return this;
+ }
+
+ void create(Map<String, String> data, Map<String, ? super Object> params)
+ throws IOException {
+ createResource("template." + dstFilePath.getFileName().toString(),
+ params)
+ .setCategory(I18N.getString(comment))
+ .setSubstitutionData(data)
+ .saveToFile(dstFilePath);
+ if (permissions != null) {
+ setPermissions(dstFilePath.toFile(), permissions);
+ }
+ }
+
+ private final Path dstFilePath;
+ private final String comment;
+ private String permissions;
+ }
+
+ private void prepareProjectConfig(Map<String, String> data,
+ Map<String, ? super Object> params) throws IOException {
+
+ Path configDir = createMetaPackage(params).sourceRoot().resolve("DEBIAN");
+ List<DebianFile> debianFiles = new ArrayList<>();
+ debianFiles.add(new DebianFile(
+ configDir.resolve("control"),
+ "resource.deb-control-file"));
+ debianFiles.add(new DebianFile(
+ configDir.resolve("preinst"),
+ "resource.deb-preinstall-script").setExecutable());
+ debianFiles.add(new DebianFile(
+ configDir.resolve("prerm"),
+ "resource.deb-prerm-script").setExecutable());
+ debianFiles.add(new DebianFile(
+ configDir.resolve("postinst"),
+ "resource.deb-postinstall-script").setExecutable());
+ debianFiles.add(new DebianFile(
+ configDir.resolve("postrm"),
+ "resource.deb-postrm-script").setExecutable());
+
+ if (!StandardBundlerParam.isRuntimeInstaller(params)) {
+ debianFiles.add(new DebianFile(
+ getConfig_CopyrightFile(params).toPath(),
+ "resource.copyright-file"));
+ }
+
+ for (DebianFile debianFile : debianFiles) {
+ debianFile.create(data, params);
+ }
+ }
+
+ @Override
+ protected Map<String, String> createReplacementData(
+ Map<String, ? super Object> params) throws IOException {
+ Map<String, String> data = new HashMap<>();
+
+ data.put("APPLICATION_MAINTAINER", MAINTAINER.fetchFrom(params));
+ data.put("APPLICATION_SECTION", SECTION.fetchFrom(params));
+ data.put("APPLICATION_COPYRIGHT", COPYRIGHT.fetchFrom(params));
+ data.put("APPLICATION_LICENSE_TEXT", LICENSE_TEXT.fetchFrom(params));
+ data.put("APPLICATION_ARCH", DEB_ARCH);
+ data.put("APPLICATION_INSTALLED_SIZE", Long.toString(
+ createMetaPackage(params).sourceApplicationLayout().sizeInBytes() >> 10));
+
+ return data;
+ }
+
+ private File getConfig_CopyrightFile(Map<String, ? super Object> params) {
+ PlatformPackage thePackage = createMetaPackage(params);
+ return thePackage.sourceRoot().resolve(Path.of(".",
+ LINUX_INSTALL_DIR.fetchFrom(params), PACKAGE_NAME.fetchFrom(
+ params), "share/doc/copyright")).toFile();
+ }
+
+ private File buildDeb(Map<String, ? super Object> params,
+ File outdir) throws IOException {
+ File outFile = new File(outdir,
+ FULL_PACKAGE_NAME.fetchFrom(params)+".deb");
+ Log.verbose(MessageFormat.format(I18N.getString(
+ "message.outputting-to-location"), outFile.getAbsolutePath()));
+
+ PlatformPackage thePackage = createMetaPackage(params);
+
+ List<String> cmdline = new ArrayList<>();
+ cmdline.addAll(List.of(TOOL_FAKEROOT, TOOL_DPKG_DEB));
+ if (Log.isVerbose()) {
+ cmdline.add("--verbose");
+ }
+ cmdline.addAll(List.of("-b", thePackage.sourceRoot().toString(),
+ outFile.getAbsolutePath()));
+
+ // run dpkg
+ Executor.of(cmdline.toArray(String[]::new)).executeExpectSuccess();
+
+ Log.verbose(MessageFormat.format(I18N.getString(
+ "message.output-to-location"), outFile.getAbsolutePath()));
+
+ return outFile;
+ }
+
+ @Override
+ public String getName() {
+ return I18N.getString("deb.bundler.name");
+ }
+
+ @Override
+ public String getID() {
+ return "deb";
+ }
+
+ @Override
+ public boolean supported(boolean runtimeInstaller) {
+ return Platform.isLinux() && (new ToolValidator(TOOL_DPKG_DEB).validate() == null);
+ }
+
+ @Override
+ public boolean isDefault() {
+ return isDebian();
+ }
+}