8231862: Decouple DesktopIntegration and LinuxPackageBundler classes
Submitted-by: asemenyuk
Reviewed-by: aherrick, almatvee
/*
* 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.jpackage.internal;
import java.io.*;
import java.nio.charset.StandardCharsets;
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.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR;
import static jdk.jpackage.internal.OverridableResource.createResource;
import static jdk.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.lines(Path.of(licenseFile),
StandardCharsets.UTF_8).collect(
Collectors.joining("\n"));
}
} 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();
});
}
/*
* 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());
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();
}
}