8231280: Linux packages produced by jpackage should have correct dependencies
Submitted-by: asemenyuk
Reviewed-by: herrick
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LibProvidersLookup.java Mon Sep 30 19:33:13 2019 -0400
@@ -0,0 +1,163 @@
+/*
+ * Copyright (c) 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.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.function.Predicate;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Builds list of packages providing dynamic libraries for the given set of files.
+ */
+final public class LibProvidersLookup {
+ static boolean supported() {
+ return (new ToolValidator(TOOL_LDD).validate() == null);
+ }
+
+ public LibProvidersLookup() {
+ }
+
+ LibProvidersLookup setPackageLookup(PackageLookup v) {
+ packageLookup = v;
+ return this;
+ }
+
+ List<String> execute(Path root) throws IOException {
+ // Get the list of files in the root for which to look up for needed shared libraries
+ List<Path> allPackageFiles = Files.walk(root).filter(
+ Files::isRegularFile).filter(LibProvidersLookup::canDependOnLibs).collect(
+ Collectors.toList());
+
+ Collection<Path> neededLibs = getNeededLibsForFiles(allPackageFiles);
+
+ // Get the list of unique package names.
+ List<String> neededPackages = neededLibs.stream().map(libPath -> {
+ try {
+ List<String> packageNames = packageLookup.apply(libPath).filter(
+ Objects::nonNull).filter(Predicate.not(String::isBlank)).distinct().collect(
+ Collectors.toList());
+ Log.verbose(String.format("%s is provided by %s", libPath, packageNames));
+ return packageNames;
+ } catch (IOException ex) {
+ // Ignore and keep going
+ Log.verbose(ex);
+ List<String> packageNames = Collections.emptyList();
+ return packageNames;
+ }
+ }).flatMap(List::stream).sorted().distinct().collect(Collectors.toList());
+
+ return neededPackages;
+ }
+
+ private static List<Path> getNeededLibsForFile(Path path) throws IOException {
+ List<Path> result = new ArrayList<>();
+ int ret = Executor.of(TOOL_LDD, path.toString()).setOutputConsumer(lines -> {
+ lines.map(line -> {
+ Matcher matcher = LIB_IN_LDD_OUTPUT_REGEX.matcher(line);
+ if (matcher.find()) {
+ return matcher.group(1);
+ }
+ return null;
+ }).filter(Objects::nonNull).map(Path::of).forEach(result::add);
+ }).execute();
+
+ if (ret != 0) {
+ // objdump failed. This is OK if the tool was applied to not a binary file
+ return Collections.emptyList();
+ }
+
+ return result;
+ }
+
+ private static Collection<Path> getNeededLibsForFiles(List<Path> paths) {
+ // Depending on tool used, the set can contain full paths (ldd) or
+ // only file names (objdump).
+ Set<Path> allLibs = paths.stream().map(path -> {
+ List<Path> libs;
+ try {
+ libs = getNeededLibsForFile(path);
+ } catch (IOException ex) {
+ Log.verbose(ex);
+ libs = Collections.emptyList();
+ }
+ return libs;
+ }).flatMap(List::stream).collect(Collectors.toSet());
+
+ // `allLibs` contains names of all .so needed by files from `paths` list.
+ // If there are mutual dependencies between binaries from `paths` list,
+ // then names or full paths to these binaries are in `allLibs` set.
+ // Remove these items from `allLibs`.
+ Set<Path> excludedNames = paths.stream().map(Path::getFileName).collect(
+ Collectors.toSet());
+ Iterator<Path> it = allLibs.iterator();
+ while (it.hasNext()) {
+ Path libName = it.next().getFileName();
+ if (excludedNames.contains(libName)) {
+ it.remove();
+ }
+ }
+
+ return allLibs;
+ }
+
+ private static boolean canDependOnLibs(Path path) {
+ return path.toFile().canExecute() || path.toString().endsWith(".so");
+ }
+
+ @FunctionalInterface
+ public interface PackageLookup {
+ Stream<String> apply(Path path) throws IOException;
+ }
+
+ private PackageLookup packageLookup;
+
+ private static final String TOOL_LDD = "ldd";
+
+ //
+ // Typical ldd output:
+ //
+ // ldd: warning: you do not have execution permission for `/tmp/jdk.jpackage17911687595930080396/images/opt/simplepackagetest/lib/runtime/lib/libawt_headless.so'
+ // linux-vdso.so.1 => (0x00007ffce6bfd000)
+ // libawt.so => /tmp/jdk.jpackage17911687595930080396/images/opt/simplepackagetest/lib/runtime/lib/libawt.so (0x00007f4e00c75000)
+ // libjvm.so => not found
+ // libjava.so => /tmp/jdk.jpackage17911687595930080396/images/opt/simplepackagetest/lib/runtime/lib/libjava.so (0x00007f4e00c41000)
+ // libm.so.6 => /lib64/libm.so.6 (0x00007f4e00834000)
+ // libdl.so.2 => /lib64/libdl.so.2 (0x00007f4e00630000)
+ // libc.so.6 => /lib64/libc.so.6 (0x00007f4e00262000)
+ // libjvm.so => not found
+ // libjvm.so => not found
+ // libverify.so => /tmp/jdk.jpackage17911687595930080396/images/opt/simplepackagetest/lib/runtime/lib/libverify.so (0x00007f4e00c2e000)
+ // /lib64/ld-linux-x86-64.so.2 (0x00007f4e00b36000)
+ // libjvm.so => not found
+ //
+ private static final Pattern LIB_IN_LDD_OUTPUT_REGEX = Pattern.compile(
+ "^\\s*\\S+\\s*=>\\s*(\\S+)\\s+\\(0[xX]\\p{XDigit}+\\)");
+}
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppBundler.java Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppBundler.java Mon Sep 30 19:33:13 2019 -0400
@@ -39,9 +39,6 @@
public class LinuxAppBundler extends AbstractImageBundler {
- private static final ResourceBundle I18N = ResourceBundle.getBundle(
- "jdk.jpackage.internal.resources.LinuxResources");
-
static final BundlerParamInfo<File> ICON_PNG =
new StandardBundlerParam<>(
"icon.png",
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppImageBuilder.java Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppImageBuilder.java Mon Sep 30 19:33:13 2019 -0400
@@ -42,9 +42,6 @@
public class LinuxAppImageBuilder extends AbstractAppImageBuilder {
- private static final ResourceBundle I18N = ResourceBundle.getBundle(
- "jdk.jpackage.internal.resources.LinuxResources");
-
private static final String LIBRARY_NAME = "libapplauncher.so";
final static String DEFAULT_ICON = "java32.png";
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java Mon Sep 30 19:33:13 2019 -0400
@@ -37,12 +37,14 @@
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.StandardBundlerParam.*;
-import static jdk.jpackage.internal.LinuxPackageBundler.I18N;
+
public class LinuxDebBundler extends LinuxPackageBundler {
@@ -82,18 +84,29 @@
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 -> {
- try {
- return PACKAGE_NAME.fetchFrom(params)
+ return PACKAGE_NAME.fetchFrom(params)
+ "_" + VERSION.fetchFrom(params)
+ "-" + RELEASE.fetchFrom(params)
- + "_" + getDebArch();
- } catch (IOException ex) {
- Log.verbose(ex);
- return null;
- }
+ + "_" + DEB_ARCH;
}, (s, p) -> s);
private static final BundlerParamInfo<String> EMAIL =
@@ -126,39 +139,17 @@
try {
String licenseFile = LICENSE_FILE.fetchFrom(params);
if (licenseFile != null) {
- StringBuilder contentBuilder = new StringBuilder();
- try (Stream<String> stream = Files.lines(Path.of(
- licenseFile), StandardCharsets.UTF_8)) {
- stream.forEach(s -> contentBuilder.append(s).append(
- "\n"));
- }
- return contentBuilder.toString();
+ return Files.lines(Path.of(licenseFile),
+ StandardCharsets.UTF_8).collect(
+ Collectors.joining("\n"));
}
- } catch (Exception e) {
+ } catch (IOException e) {
Log.verbose(e);
}
return "Unknown";
},
(s, p) -> s);
- private final static String TOOL_DPKG_DEB = "dpkg-deb";
- private final static String TOOL_DPKG = "dpkg";
-
- public static boolean testTool(String toolName, String minVersion) {
- try {
- ProcessBuilder pb = new ProcessBuilder(
- toolName,
- "--version");
- // not interested in the output
- IOUtils.exec(pb, true, null);
- } catch (Exception e) {
- Log.verbose(MessageFormat.format(I18N.getString(
- "message.test-for-tool"), toolName, e.getMessage()));
- return false;
- }
- return true;
- }
-
public LinuxDebBundler() {
super(PACKAGE_NAME);
}
@@ -166,28 +157,21 @@
@Override
public void doValidate(Map<String, ? super Object> params)
throws ConfigException {
- // NOTE: Can we validate that the required tools are available
- // before we start?
- if (!testTool(TOOL_DPKG_DEB, "1")){
- throw new ConfigException(MessageFormat.format(
- I18N.getString("error.tool-not-found"), TOOL_DPKG_DEB),
- I18N.getString("error.tool-not-found.advice"));
- }
- if (!testTool(TOOL_DPKG, "1")){
- throw new ConfigException(MessageFormat.format(
- I18N.getString("error.tool-not-found"), TOOL_DPKG),
- I18N.getString("error.tool-not-found.advice"));
- }
-
- // Show warning is license file is missing
- String licenseFile = LICENSE_FILE.fetchFrom(params);
- if (licenseFile == null) {
+ // 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
@@ -198,6 +182,83 @@
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"
*
@@ -217,23 +278,13 @@
}
- private static String getDebArch() throws IOException {
- try (var baos = new ByteArrayOutputStream();
- var ps = new PrintStream(baos)) {
- var pb = new ProcessBuilder(TOOL_DPKG, "--print-architecture");
- IOUtils.exec(pb, false, ps);
- return baos.toString().split("\n", 2)[0];
- }
- }
-
public static boolean isDebian() {
// we are just going to run "dpkg -s coreutils" and assume Debian
// or deritive if no error is returned.
- var pb = new ProcessBuilder(TOOL_DPKG, "-s", "coreutils");
try {
- int ret = pb.start().waitFor();
- return (ret == 0);
- } catch (IOException | InterruptedException e) {
+ Executor.of(TOOL_DPKG, "-s", "coreutils").executeExpectSuccess();
+ return true;
+ } catch (IOException e) {
// just fall thru
}
return false;
@@ -340,7 +391,7 @@
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", getDebArch());
+ data.put("APPLICATION_ARCH", DEB_ARCH);
data.put("APPLICATION_INSTALLED_SIZE", Long.toString(
createMetaPackage(params).sourceApplicationLayout().sizeInBytes() >> 10));
@@ -363,12 +414,16 @@
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
- ProcessBuilder pb = new ProcessBuilder(
- "fakeroot", TOOL_DPKG_DEB, "-b",
- thePackage.sourceRoot().toString(),
- outFile.getAbsolutePath());
- IOUtils.exec(pb);
+ Executor.of(cmdline.toArray(String[]::new)).executeExpectSuccess();
Log.verbose(MessageFormat.format(I18N.getString(
"message.output-to-location"), outFile.getAbsolutePath()));
@@ -388,17 +443,11 @@
@Override
public boolean supported(boolean runtimeInstaller) {
- if (Platform.getPlatform() == Platform.LINUX) {
- if (testTool(TOOL_DPKG_DEB, "1")) {
- return true;
- }
- }
- return false;
+ return Platform.isLinux() && (new ToolValidator(TOOL_DPKG_DEB).validate() == null);
}
@Override
public boolean isDefault() {
return isDebian();
}
-
}
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageBundler.java Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageBundler.java Mon Sep 30 19:33:13 2019 -0400
@@ -25,24 +25,12 @@
package jdk.jpackage.internal;
import java.awt.image.BufferedImage;
-import java.io.BufferedReader;
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Writer;
+import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.ResourceBundle;
+import java.util.*;
+import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.imageio.ImageIO;
@@ -58,9 +46,6 @@
abstract class LinuxPackageBundler extends AbstractBundler {
- protected static final ResourceBundle I18N = ResourceBundle.getBundle(
- "jdk.jpackage.internal.resources.LinuxResources");
-
private static final String DESKTOP_COMMANDS_INSTALL = "DESKTOP_COMMANDS_INSTALL";
private static final String DESKTOP_COMMANDS_UNINSTALL = "DESKTOP_COMMANDS_UNINSTALL";
private static final String UTILITY_SCRIPTS = "UTILITY_SCRIPTS";
@@ -95,37 +80,47 @@
}
private final BundlerParamInfo<String> packageName;
+ private boolean withFindNeededPackages;
@Override
final public boolean validate(Map<String, ? super Object> params)
throws ConfigException {
- try {
- if (params == null) throw new ConfigException(
- I18N.getString("error.parameters-null"),
- I18N.getString("error.parameters-null.advice"));
- // run basic validation to ensure requirements are met
- // we are not interested in return code, only possible exception
- APP_BUNDLER.fetchFrom(params).validate(params);
+ // run basic validation to ensure requirements are met
+ // we are not interested in return code, only possible exception
+ APP_BUNDLER.fetchFrom(params).validate(params);
- validateFileAssociations(FILE_ASSOCIATIONS.fetchFrom(params));
+ validateFileAssociations(FILE_ASSOCIATIONS.fetchFrom(params));
- // If package name has some restrictions, the string converter will
- // throw an exception if invalid
- packageName.getStringConverter().apply(packageName.fetchFrom(params),
- params);
+ // If package name has some restrictions, the string converter will
+ // throw an exception if invalid
+ packageName.getStringConverter().apply(packageName.fetchFrom(params),
+ params);
- // Packaging specific validation
- doValidate(params);
-
- return true;
- } catch (RuntimeException re) {
- if (re.getCause() instanceof ConfigException) {
- throw (ConfigException) re.getCause();
- } else {
- throw new ConfigException(re);
+ for (var validator: getToolValidators(params)) {
+ ConfigException ex = validator.validate();
+ if (ex != null) {
+ throw ex;
}
}
+
+ withFindNeededPackages = LibProvidersLookup.supported();
+ if (!withFindNeededPackages) {
+ final String advice;
+ if ("deb".equals(getID())) {
+ advice = "message.deb-ldd-not-available.advice";
+ } else {
+ advice = "message.rpm-ldd-not-available.advice";
+ }
+ // Let user know package dependencies will not be generated.
+ Log.error(String.format("%s\n%s", I18N.getString(
+ "message.ldd-not-available"), I18N.getString(advice)));
+ }
+
+ // Packaging specific validation
+ doValidate(params);
+
+ return true;
}
@Override
@@ -168,13 +163,18 @@
}
}
+ if (!StandardBundlerParam.isRuntimeInstaller(params)) {
+ desktopIntegration = new DesktopIntegration(thePackage, params);
+ } else {
+ desktopIntegration = null;
+ }
+
Map<String, String> data = createDefaultReplacementData(params);
- if (StandardBundlerParam.isRuntimeInstaller(params)) {
+ if (desktopIntegration != null) {
+ data.putAll(desktopIntegration.create());
+ } else {
Stream.of(DESKTOP_COMMANDS_INSTALL, DESKTOP_COMMANDS_UNINSTALL,
UTILITY_SCRIPTS).forEach(v -> data.put(v, ""));
- } else {
- data.putAll(
- new DesktopIntegration(thePackage, params).prepareForApplication());
}
data.putAll(createReplacementData(params));
@@ -187,6 +187,39 @@
}
}
+ private List<String> getListOfNeededPackages(
+ Map<String, ? super Object> params) throws IOException {
+
+ PlatformPackage thePackage = createMetaPackage(params);
+
+ final List<String> xdgUtilsPackage;
+ if (desktopIntegration != null) {
+ xdgUtilsPackage = desktopIntegration.requiredPackages();
+ } else {
+ xdgUtilsPackage = Collections.emptyList();
+ }
+
+ final List<String> neededLibPackages;
+ if (withFindNeededPackages) {
+ LibProvidersLookup lookup = new LibProvidersLookup();
+ initLibProvidersLookup(params, lookup);
+
+ neededLibPackages = lookup.execute(thePackage.sourceRoot());
+ } else {
+ neededLibPackages = Collections.emptyList();
+ }
+
+ // Merge all package lists together.
+ // Filter out empty names, sort and remove duplicates.
+ List<String> result = Stream.of(xdgUtilsPackage, neededLibPackages).flatMap(
+ List::stream).filter(Predicate.not(String::isEmpty)).sorted().distinct().collect(
+ Collectors.toList());
+
+ Log.verbose(String.format("Required packages: %s", result));
+
+ return result;
+ }
+
private Map<String, String> createDefaultReplacementData(
Map<String, ? super Object> params) throws IOException {
Map<String, String> data = new HashMap<>();
@@ -196,13 +229,26 @@
data.put("APPLICATION_VERSION", VERSION.fetchFrom(params));
data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params));
data.put("APPLICATION_RELEASE", RELEASE.fetchFrom(params));
- data.put("PACKAGE_DEPENDENCIES", LINUX_PACKAGE_DEPENDENCIES.fetchFrom(
- params));
+
+ String defaultDeps = String.join(", ", getListOfNeededPackages(params));
+ String customDeps = LINUX_PACKAGE_DEPENDENCIES.fetchFrom(params).strip();
+ if (!customDeps.isEmpty() && !defaultDeps.isEmpty()) {
+ customDeps = ", " + customDeps;
+ }
+ data.put("PACKAGE_DEFAULT_DEPENDENCIES", defaultDeps);
+ data.put("PACKAGE_CUSTOM_DEPENDENCIES", customDeps);
return data;
}
- abstract void doValidate(Map<String, ? super Object> params)
+ abstract protected void initLibProvidersLookup(
+ Map<String, ? super Object> params,
+ LibProvidersLookup libProvidersLookup);
+
+ abstract protected List<ToolValidator> getToolValidators(
+ Map<String, ? super Object> params);
+
+ abstract protected void doValidate(Map<String, ? super Object> params)
throws ConfigException;
abstract protected Map<String, String> createReplacementData(
@@ -333,11 +379,21 @@
iconFile = null;
}
- this.desktopFileData = Collections.unmodifiableMap(
+ desktopFileData = Collections.unmodifiableMap(
createDataForDesktopFile(params));
+
+ nestedIntegrations = launchers.stream().map(
+ launcherParams -> new DesktopIntegration(thePackage,
+ launcherParams)).collect(Collectors.toList());
}
- Map<String, String> prepareForApplication() throws IOException {
+ List<String> requiredPackages() {
+ return Stream.of(List.of(this), nestedIntegrations).flatMap(
+ List::stream).map(DesktopIntegration::requiredPackagesSelf).flatMap(
+ List::stream).distinct().collect(Collectors.toList());
+ }
+
+ Map<String, String> create() throws IOException {
if (iconFile != null) {
// Create application icon file.
prepareSrcIconFile();
@@ -386,15 +442,12 @@
data.get(DESKTOP_COMMANDS_INSTALL)));
List<String> uninstallShellCmds = new ArrayList<>(Arrays.asList(
data.get(DESKTOP_COMMANDS_UNINSTALL)));
- for (Map<String, ? super Object> params : launchers) {
- DesktopIntegration integration = new DesktopIntegration(
- thePackage, params);
-
+ for (var integration: nestedIntegrations) {
if (!integration.associations.isEmpty()) {
needCleanupScripts = true;
}
- Map<String, String> launcherData = integration.prepareForApplication();
+ Map<String, String> launcherData = integration.create();
installShellCmds.add(launcherData.get(DESKTOP_COMMANDS_INSTALL));
uninstallShellCmds.add(launcherData.get(
@@ -421,6 +474,13 @@
return data;
}
+ private List<String> requiredPackagesSelf() {
+ if (desktopFile != null) {
+ return List.of("xdg-utils");
+ }
+ return Collections.emptyList();
+ }
+
private Map<String, String> createDataForDesktopFile(
Map<String, ? super Object> params) {
Map<String, String> data = new HashMap<>();
@@ -558,6 +618,8 @@
private final DesktopFile desktopFile;
private final DesktopFile iconFile;
+ final private List<DesktopIntegration> nestedIntegrations;
+
private final Map<String, String> desktopFileData;
/**
@@ -709,4 +771,6 @@
return String.join(System.lineSeparator(), commands.stream().filter(
s -> s != null && !s.isEmpty()).collect(Collectors.toList()));
}
+
+ private DesktopIntegration desktopIntegration;
}
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmBundler.java Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmBundler.java Mon Sep 30 19:33:13 2019 -0400
@@ -32,6 +32,7 @@
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import java.util.stream.Collectors;
import static jdk.jpackage.internal.StandardBundlerParam.*;
import static jdk.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR;
@@ -103,32 +104,9 @@
private final static String DEFAULT_SPEC_TEMPLATE = "template.spec";
+ public final static String TOOL_RPM = "rpm";
public final static String TOOL_RPMBUILD = "rpmbuild";
- public final static double TOOL_RPMBUILD_MIN_VERSION = 4.0d;
-
- public static boolean testTool(String toolName, double minVersion) {
- try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
- PrintStream ps = new PrintStream(baos)) {
- ProcessBuilder pb = new ProcessBuilder(toolName, "--version");
- IOUtils.exec(pb, false, ps);
- //not interested in the above's output
- String content = new String(baos.toByteArray());
- Pattern pattern = Pattern.compile(" (\\d+\\.\\d+)");
- Matcher matcher = pattern.matcher(content);
-
- if (matcher.find()) {
- String v = matcher.group(1);
- double version = Double.parseDouble(v);
- return minVersion <= version;
- } else {
- return false;
- }
- } catch (Exception e) {
- Log.verbose(MessageFormat.format(I18N.getString(
- "message.test-for-tool"), toolName, e.getMessage()));
- return false;
- }
- }
+ public final static String TOOL_RPMBUILD_MIN_VERSION = "4.0";
public LinuxRpmBundler() {
super(PACKAGE_NAME);
@@ -137,20 +115,26 @@
@Override
public void doValidate(Map<String, ? super Object> params)
throws ConfigException {
- if (params == null) throw new ConfigException(
- I18N.getString("error.parameters-null"),
- I18N.getString("error.parameters-null.advice"));
+ }
- // validate presense of required tools
- if (!testTool(TOOL_RPMBUILD, TOOL_RPMBUILD_MIN_VERSION)){
- throw new ConfigException(
- MessageFormat.format(
- I18N.getString("error.cannot-find-rpmbuild"),
- TOOL_RPMBUILD_MIN_VERSION),
- MessageFormat.format(
- I18N.getString("error.cannot-find-rpmbuild.advice"),
- TOOL_RPMBUILD_MIN_VERSION));
- }
+ private static ToolValidator createRpmbuildToolValidator() {
+ Pattern pattern = Pattern.compile(" (\\d+\\.\\d+)");
+ return new ToolValidator(TOOL_RPMBUILD).setMinimalVersion(
+ TOOL_RPMBUILD_MIN_VERSION).setVersionParser(lines -> {
+ String versionString = lines.limit(1).collect(
+ Collectors.toList()).get(0);
+ Matcher matcher = pattern.matcher(versionString);
+ if (matcher.find()) {
+ return matcher.group(1);
+ }
+ return null;
+ });
+ }
+
+ @Override
+ protected List<ToolValidator> getToolValidators(
+ Map<String, ? super Object> params) {
+ return List.of(createRpmbuildToolValidator());
}
@Override
@@ -193,6 +177,18 @@
return data;
}
+ @Override
+ protected void initLibProvidersLookup(
+ Map<String, ? super Object> params,
+ LibProvidersLookup libProvidersLookup) {
+ libProvidersLookup.setPackageLookup(file -> {
+ return Executor.of(TOOL_RPM,
+ "-q", "--queryformat", "%{name}\\n",
+ "-q", "--whatprovides", file.toString())
+ .saveOutput(true).executeExpectSuccess().getOutput().stream();
+ });
+ }
+
private Path specFile(Map<String, ? super Object> params) {
return TEMP_ROOT.fetchFrom(params).toPath().resolve(Path.of("SPECS",
PACKAGE_NAME.fetchFrom(params) + ".spec"));
@@ -207,17 +203,18 @@
PlatformPackage thePackage = createMetaPackage(params);
//run rpmbuild
- ProcessBuilder pb = new ProcessBuilder(
+ Executor.of(
TOOL_RPMBUILD,
"-bb", specFile(params).toAbsolutePath().toString(),
- "--define", String.format("%%_sourcedir %s", thePackage.sourceRoot()),
+ "--define", String.format("%%_sourcedir %s",
+ thePackage.sourceRoot()),
// save result to output dir
- "--define", String.format("%%_rpmdir %s", outdir.getAbsolutePath()),
+ "--define", String.format("%%_rpmdir %s",
+ outdir.getAbsolutePath()),
// do not use other system directories to build as current user
"--define", String.format("%%_topdir %s",
TEMP_ROOT.fetchFrom(params).toPath().toAbsolutePath())
- );
- IOUtils.exec(pb);
+ ).executeExpectSuccess();
Log.verbose(MessageFormat.format(
I18N.getString("message.output-bundle-location"),
@@ -253,12 +250,7 @@
@Override
public boolean supported(boolean runtimeInstaller) {
- if (Platform.getPlatform() == Platform.LINUX) {
- if (testTool(TOOL_RPMBUILD, TOOL_RPMBUILD_MIN_VERSION)) {
- return true;
- }
- }
- return false;
+ return Platform.isLinux() && (createRpmbuildToolValidator().validate() == null);
}
@Override
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/LinuxResources.properties Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/LinuxResources.properties Mon Sep 30 19:33:13 2019 -0400
@@ -42,14 +42,14 @@
error.parameters-null=Parameters map is null
error.parameters-null.advice=Pass in a non-null parameters map
-error.tool-not-found=Can not find {0}
+
error.tool-not-found.advice=Please install required packages
+error.tool-old-version.advice=Please install required packages
+
error.no-content-types-for-file-association=No MIME types were specified for File Association number {0}
error.too-many-content-types-for-file-association=More than one MIME types was specified for File Association number {0}.
error.invalid-value-for-package-name=Invalid value "{0}" for the bundle name.
error.invalid-value-for-package-name.advice=Set the "linux-bundle-name" option to a valid Debian package name. Note that the 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.
-error.cannot-find-rpmbuild=Can not find rpmbuild {0} or newer
-error.cannot-find-rpmbuild.advice=\ Install packages needed to build RPM, version {0} or newer
message.icon-not-png=The specified icon "{0}" is not a PNG file and will not be used. The default icon will be used in it's place.
message.test-for-tool=Test for [{0}]. Result: {1}
@@ -59,3 +59,7 @@
message.outputting-bundle-location=Generating RPM for installer to: {0}.
message.output-bundle-location=Package (.rpm) saved to: {0}.
message.creating-association-with-null-extension=Creating association with null extension.
+
+message.ldd-not-available=ldd command not found. Package dependencies will not be generated.
+message.deb-ldd-not-available.advice=Install "libc-bin" DEB package to get ldd.
+message.rpm-ldd-not-available.advice=Install "glibc-common" RPM package to get ldd.
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/LinuxResources_ja.properties Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/LinuxResources_ja.properties Mon Sep 30 19:33:13 2019 -0400
@@ -42,14 +42,14 @@
error.parameters-null=Parameters map is null
error.parameters-null.advice=Pass in a non-null parameters map
-error.tool-not-found=Can not find {0}
+
error.tool-not-found.advice=Please install required packages
+error.tool-old-version.advice=Please install required packages
+
error.no-content-types-for-file-association=No MIME types were specified for File Association number {0}
error.too-many-content-types-for-file-association=More than one MIME types was specified for File Association number {0}.
error.invalid-value-for-package-name=Invalid value "{0}" for the bundle name.
error.invalid-value-for-package-name.advice=Set the "linux-bundle-name" option to a valid Debian package name. Note that the 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.
-error.cannot-find-rpmbuild=Can not find rpmbuild {0} or newer
-error.cannot-find-rpmbuild.advice=\ Install packages needed to build RPM, version {0} or newer
message.icon-not-png=The specified icon "{0}" is not a PNG file and will not be used. The default icon will be used in it's place.
message.test-for-tool=Test for [{0}]. Result: {1}
@@ -59,3 +59,7 @@
message.outputting-bundle-location=Generating RPM for installer to: {0}.
message.output-bundle-location=Package (.rpm) saved to: {0}.
message.creating-association-with-null-extension=Creating association with null extension.
+
+message.ldd-not-available=ldd command not found. Package dependencies will not be generated.
+message.deb-ldd-not-available.advice=Install "libc-bin" DEB package to get ldd.
+message.rpm-ldd-not-available.advice=Install "glibc-common" RPM package to get ldd.
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/LinuxResources_zh_CN.properties Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/LinuxResources_zh_CN.properties Mon Sep 30 19:33:13 2019 -0400
@@ -42,14 +42,14 @@
error.parameters-null=Parameters map is null
error.parameters-null.advice=Pass in a non-null parameters map
-error.tool-not-found=Can not find {0}
+
error.tool-not-found.advice=Please install required packages
+error.tool-old-version.advice=Please install required packages
+
error.no-content-types-for-file-association=No MIME types were specified for File Association number {0}
error.too-many-content-types-for-file-association=More than one MIME types was specified for File Association number {0}.
error.invalid-value-for-package-name=Invalid value "{0}" for the bundle name.
error.invalid-value-for-package-name.advice=Set the "linux-bundle-name" option to a valid Debian package name. Note that the 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.
-error.cannot-find-rpmbuild=Can not find rpmbuild {0} or newer
-error.cannot-find-rpmbuild.advice=\ Install packages needed to build RPM, version {0} or newer
message.icon-not-png=The specified icon "{0}" is not a PNG file and will not be used. The default icon will be used in it's place.
message.test-for-tool=Test for [{0}]. Result: {1}
@@ -59,3 +59,7 @@
message.outputting-bundle-location=Generating RPM for installer to: {0}.
message.output-bundle-location=Package (.rpm) saved to: {0}.
message.creating-association-with-null-extension=Creating association with null extension.
+
+message.ldd-not-available=ldd command not found. Package dependencies will not be generated.
+message.deb-ldd-not-available.advice=Install "libc-bin" DEB package to get ldd.
+message.rpm-ldd-not-available.advice=Install "glibc-common" RPM package to get ldd.
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.control Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.control Mon Sep 30 19:33:13 2019 -0400
@@ -6,5 +6,5 @@
Architecture: APPLICATION_ARCH
Provides: APPLICATION_PACKAGE
Description: APPLICATION_DESCRIPTION
-Depends: PACKAGE_DEPENDENCIES
+Depends: PACKAGE_DEFAULT_DEPENDENCIES PACKAGE_CUSTOM_DEPENDENCIES
Installed-Size: APPLICATION_INSTALLED_SIZE
--- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.spec Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.spec Mon Sep 30 19:33:13 2019 -0400
@@ -12,8 +12,8 @@
Autoprov: 0
Autoreq: 0
-%if "xPACKAGE_DEPENDENCIES" != x
-Requires: PACKAGE_DEPENDENCIES
+%if "xPACKAGE_DEFAULT_DEPENDENCIES" != x || "xPACKAGE_CUSTOM_DEPENDENCIES" != x
+Requires: PACKAGE_DEFAULT_DEPENDENCIES PACKAGE_CUSTOM_DEPENDENCIES
%endif
#avoid ARCH subfolder
--- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ConfigException.java Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ConfigException.java Mon Sep 30 19:33:13 2019 -0400
@@ -34,6 +34,11 @@
this.advice = advice;
}
+ public ConfigException(String msg, String advice, Exception cause) {
+ super(msg, cause);
+ this.advice = advice;
+ }
+
public ConfigException(Exception cause) {
super(cause);
this.advice = null;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Executor.java Mon Sep 30 19:33:13 2019 -0400
@@ -0,0 +1,162 @@
+/*
+ * Copyright (c) 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.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+final public class Executor {
+
+ Executor() {
+ }
+
+ Executor setOutputConsumer(Consumer<Stream<String>> v) {
+ outputConsumer = v;
+ return this;
+ }
+
+ Executor saveOutput(boolean v) {
+ saveOutput = v;
+ return this;
+ }
+
+ Executor setProcessBuilder(ProcessBuilder v) {
+ pb = v;
+ return this;
+ }
+
+ Executor setCommandLine(String... cmdline) {
+ return setProcessBuilder(new ProcessBuilder(cmdline));
+ }
+
+ List<String> getOutput() {
+ return output;
+ }
+
+ Executor executeExpectSuccess() throws IOException {
+ int ret = execute();
+ if (0 != ret) {
+ throw new IOException(
+ String.format("Command %s exited with %d code",
+ createLogMessage(pb), ret));
+ }
+ return this;
+ }
+
+ int execute() throws IOException {
+ output = null;
+
+ boolean needProcessOutput = outputConsumer != null || Log.isVerbose() || saveOutput;
+ if (needProcessOutput) {
+ pb.redirectErrorStream(true);
+ } else {
+ // We are not going to read process output, so need to notify
+ // ProcessBuilder about this. Otherwise some processes might just
+ // hang up (`ldconfig -p`).
+ pb.redirectError(ProcessBuilder.Redirect.DISCARD);
+ pb.redirectOutput(ProcessBuilder.Redirect.DISCARD);
+ }
+
+ Log.verbose(String.format("Running %s", createLogMessage(pb)));
+ Process p = pb.start();
+
+ if (needProcessOutput) {
+ try (var br = new BufferedReader(new InputStreamReader(
+ p.getInputStream()))) {
+ final List<String> savedOutput;
+ // Need to save output if explicitely requested (saveOutput=true) or
+ // if will be used used by multiple consumers
+ if ((outputConsumer != null && Log.isVerbose()) || saveOutput) {
+ savedOutput = br.lines().collect(Collectors.toList());
+ if (saveOutput) {
+ output = savedOutput;
+ }
+ } else {
+ savedOutput = null;
+ }
+
+ Supplier<Stream<String>> outputStream = () -> {
+ if (savedOutput != null) {
+ return savedOutput.stream();
+ }
+ return br.lines();
+ };
+
+ if (Log.isVerbose()) {
+ outputStream.get().forEach(Log::verbose);
+ }
+
+ if (outputConsumer != null) {
+ outputConsumer.accept(outputStream.get());
+ }
+
+ if (savedOutput == null) {
+ // For some processes on Linux if the output stream
+ // of the process is opened but not consumed, the process
+ // would exit with code 141.
+ // It turned out that reading just a single line of process
+ // output fixes the problem, but let's process
+ // all of the output, just in case.
+ br.lines().forEach(x -> {});
+ }
+ }
+ }
+
+ try {
+ return p.waitFor();
+ } catch (InterruptedException ex) {
+ Log.verbose(ex);
+ throw new RuntimeException(ex);
+ }
+ }
+
+ static Executor of(String... cmdline) {
+ return new Executor().setCommandLine(cmdline);
+ }
+
+ static Executor of(ProcessBuilder pb) {
+ return new Executor().setProcessBuilder(pb);
+ }
+
+ private static String createLogMessage(ProcessBuilder pb) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(String.format("%s", pb.command()));
+ if (pb.directory() != null) {
+ sb.append(String.format("in %s", pb.directory().getAbsolutePath()));
+ }
+ return sb.toString();
+ }
+
+ private ProcessBuilder pb;
+ private boolean saveOutput;
+ private List<String> output;
+ private Consumer<Stream<String>> outputConsumer;
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/I18N.java Mon Sep 30 19:33:13 2019 -0400
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 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.util.ResourceBundle;
+
+class I18N {
+
+ static String getString(String key) {
+ if (PLATFORM.containsKey(key)) {
+ return PLATFORM.getString(key);
+ }
+ return SHARED.getString(key);
+ }
+
+ private static final ResourceBundle SHARED = ResourceBundle.getBundle(
+ "jdk.jpackage.internal.resources.MainResources");
+
+ private static final ResourceBundle PLATFORM;
+
+ static {
+ if (Platform.isLinux()) {
+ PLATFORM = ResourceBundle.getBundle(
+ "jdk.jpackage.internal.resources.LinuxResources");
+ } else if (Platform.isWindows()) {
+ PLATFORM = ResourceBundle.getBundle(
+ "jdk.jpackage.internal.resources.WinResources");
+ } else if (Platform.isMac()) {
+ PLATFORM = ResourceBundle.getBundle(
+ "jdk.jpackage.internal.resources.MacResources");
+ } else {
+ throw new IllegalStateException("Unknwon platform");
+ }
+ }
+}
--- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/IOUtils.java Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/IOUtils.java Mon Sep 30 19:33:13 2019 -0400
@@ -26,8 +26,6 @@
package jdk.jpackage.internal;
import java.io.*;
-import java.net.URL;
-import java.util.Arrays;
import java.nio.channels.FileChannel;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
@@ -150,37 +148,20 @@
exec(pb, false, null);
}
- public static void exec(ProcessBuilder pb, boolean testForPresenseOnly,
+ static void exec(ProcessBuilder pb, boolean testForPresenseOnly,
PrintStream consumer) throws IOException {
- pb.redirectErrorStream(true);
- Log.verbose("Running "
- + Arrays.toString(pb.command().toArray(new String[0]))
- + (pb.directory() != null ? (" in " + pb.directory()) : ""));
- Process p = pb.start();
- InputStreamReader isr = new InputStreamReader(p.getInputStream());
- BufferedReader br = new BufferedReader(isr);
- String lineRead;
- String output = "";
- while ((lineRead = br.readLine()) != null) {
+ List<String> output = new ArrayList<>();
+ Executor exec = Executor.of(pb).setOutputConsumer(lines -> {
+ lines.forEach(output::add);
if (consumer != null) {
- consumer.print(lineRead + '\n');
- } else {
- Log.verbose(lineRead);
+ output.forEach(consumer::println);
}
- output += lineRead;
- }
- try {
- int ret = p.waitFor();
- if (ret != 0 && !(testForPresenseOnly && ret != 127)) {
- throw new IOException("Exec failed with code "
- + ret + " command ["
- + Arrays.toString(pb.command().toArray(new String[0]))
- + " in " + (pb.directory() != null ?
- pb.directory().getAbsolutePath() :
- "unspecified directory")
- + " output: " + output);
- }
- } catch (InterruptedException ex) {
+ });
+
+ if (testForPresenseOnly) {
+ exec.execute();
+ } else {
+ exec.executeExpectSuccess();
}
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ToolValidator.java Mon Sep 30 19:33:13 2019 -0400
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 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.IOException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+
+public final class ToolValidator {
+
+ ToolValidator(String name) {
+ this.name = name;
+ args = new ArrayList<>();
+
+ if (Platform.getPlatform() == Platform.LINUX) {
+ setCommandLine("--version");
+ }
+ }
+
+ ToolValidator setCommandLine(String... args) {
+ this.args = List.of(args);
+ return this;
+ }
+
+ ToolValidator setMinimalVersion(String v) {
+ minimalVersion = v;
+ return this;
+ }
+
+ ToolValidator setVersionParser(Function<Stream<String>, String> v) {
+ versionParser = v;
+ return this;
+ }
+
+ ConfigException validate() {
+ List<String> cmdline = new ArrayList<>();
+ cmdline.add(name);
+ cmdline.addAll(args);
+ try {
+ ProcessBuilder pb = new ProcessBuilder(cmdline);
+ AtomicBoolean canUseTool = new AtomicBoolean();
+ if (minimalVersion == null) {
+ // No version check.
+ canUseTool.setPlain(true);
+ }
+
+ Executor.of(pb).setOutputConsumer(lines -> {
+ if (versionParser != null && minimalVersion != null) {
+ String version = versionParser.apply(lines);
+ if (minimalVersion.compareTo(version) < 0) {
+ canUseTool.setPlain(true);
+ }
+ }
+ }).execute();
+
+ if (!canUseTool.getPlain()) {
+ return new ConfigException(MessageFormat.format(I18N.getString(
+ "error.tool-old-version"), name, minimalVersion),
+ MessageFormat.format(I18N.getString(
+ "error.tool-old-version.advice"), name,
+ minimalVersion));
+ }
+ } catch (IOException e) {
+ return new ConfigException(MessageFormat.format(I18N.getString(
+ "error.tool-not-found"), name, e.getMessage()),
+ MessageFormat.format(I18N.getString(
+ "error.tool-not-found.advice"), name), e);
+ }
+
+ // All good. Tool can be used.
+ return null;
+ }
+
+ private final String name;
+ private List<String> args;
+ private String minimalVersion;
+ private Function<Stream<String>, String> versionParser;
+}
--- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties Mon Sep 30 19:33:13 2019 -0400
@@ -53,6 +53,11 @@
error.main-jar-does-not-exist=The configured main jar does not exist {0} in the input directory
error.main-jar-does-not-exist.advice=The main jar must be specified relative to the input directory (not an absolute path), and must exist within that directory
+error.tool-not-found=Can not find {0}. Reason: {1}
+error.tool-not-found.advice=Please install {0}
+error.tool-old-version=Can not find {0} {1} or newer
+error.tool-old-version.advice=Please install {0} {1} or newer
+
warning.module.does.not.exist=Module [{0}] does not exist
warning.no.jdk.modules.found=Warning: No JDK Modules found
--- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources_ja.properties Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources_ja.properties Mon Sep 30 19:33:13 2019 -0400
@@ -53,6 +53,11 @@
error.main-jar-does-not-exist=The configured main jar does not exist {0} in the input directory
error.main-jar-does-not-exist.advice=The main jar must be specified relative to the input directory (not an absolute path), and must exist within that directory
+error.tool-not-found=Can not find {0}. Reason: {1}
+error.tool-not-found.advice=Please install {0}
+error.tool-old-version=Can not find {0} {1} or newer
+error.tool-old-version.advice=Please install {0} {1} or newer
+
warning.module.does.not.exist=Module [{0}] does not exist
warning.no.jdk.modules.found=Warning: No JDK Modules found
--- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources_zh_CN.properties Mon Sep 30 19:11:19 2019 -0400
+++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources_zh_CN.properties Mon Sep 30 19:33:13 2019 -0400
@@ -53,6 +53,11 @@
error.main-jar-does-not-exist=The configured main jar does not exist {0} in the input directory
error.main-jar-does-not-exist.advice=The main jar must be specified relative to the input directory (not an absolute path), and must exist within that directory
+error.tool-not-found=Can not find {0}. Reason: {1}
+error.tool-not-found.advice=Please install {0}
+error.tool-old-version=Can not find {0} {1} or newer
+error.tool-old-version.advice=Please install {0} {1} or newer
+
warning.module.does.not.exist=Module [{0}] does not exist
warning.no.jdk.modules.found=Warning: No JDK Modules found
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java Mon Sep 30 19:11:19 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java Mon Sep 30 19:33:13 2019 -0400
@@ -216,6 +216,10 @@
} else if (saveOutputType.contains(SaveOutputType.DUMP)) {
builder.inheritIO();
sb.append("; inherit I/O");
+ } else {
+ builder.redirectError(ProcessBuilder.Redirect.DISCARD);
+ builder.redirectOutput(ProcessBuilder.Redirect.DISCARD);
+ sb.append("; discard I/O");
}
if (directory != null) {
builder.directory(directory.toFile());
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java Mon Sep 30 19:11:19 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java Mon Sep 30 19:33:13 2019 -0400
@@ -191,7 +191,66 @@
.executeAndGetFirstLineOfOutput();
}
- static void addDebBundleDesktopIntegrationVerifier(PackageTest test,
+ static void verifyPackageBundleEssential(JPackageCommand cmd) {
+ String packageName = LinuxHelper.getPackageName(cmd);
+ TKit.assertNotEquals(0L, LinuxHelper.getInstalledPackageSizeKB(
+ cmd), String.format(
+ "Check installed size of [%s] package in KB is not zero",
+ packageName));
+
+ final boolean checkPrerequisites;
+ if (cmd.isRuntime()) {
+ Path runtimeDir = cmd.appRuntimeInstallationDirectory();
+ Set<Path> expectedCriticalRuntimePaths = CRITICAL_RUNTIME_FILES.stream().map(
+ runtimeDir::resolve).collect(Collectors.toSet());
+ Set<Path> actualCriticalRuntimePaths = getPackageFiles(cmd).filter(
+ expectedCriticalRuntimePaths::contains).collect(
+ Collectors.toSet());
+ checkPrerequisites = expectedCriticalRuntimePaths.equals(
+ actualCriticalRuntimePaths);
+ } else {
+ checkPrerequisites = true;
+ }
+
+ List<String> prerequisites = LinuxHelper.getPrerequisitePackages(cmd);
+ if (checkPrerequisites) {
+ final String vitalPackage = "libc";
+ TKit.assertTrue(prerequisites.stream().filter(
+ dep -> dep.contains(vitalPackage)).findAny().isPresent(),
+ String.format(
+ "Check [%s] package is in the list of required packages %s of [%s] package",
+ vitalPackage, prerequisites, packageName));
+ } else {
+ TKit.trace(String.format(
+ "Not cheking %s required packages of [%s] package",
+ prerequisites, packageName));
+ }
+ }
+
+ static void addBundleDesktopIntegrationVerifier(PackageTest test,
+ boolean integrated) {
+ final String xdgUtils = "xdg-utils";
+
+ test.addBundleVerifier(cmd -> {
+ List<String> prerequisites = getPrerequisitePackages(cmd);
+ boolean xdgUtilsFound = prerequisites.contains(xdgUtils);
+ if (integrated) {
+ TKit.assertTrue(xdgUtilsFound, String.format(
+ "Check [%s] is in the list of required packages %s",
+ xdgUtils, prerequisites));
+ } else {
+ TKit.assertFalse(xdgUtilsFound, String.format(
+ "Check [%s] is NOT in the list of required packages %s",
+ xdgUtils, prerequisites));
+ }
+ });
+
+ test.forTypes(PackageType.LINUX_DEB, () -> {
+ addDebBundleDesktopIntegrationVerifier(test, integrated);
+ });
+ }
+
+ private static void addDebBundleDesktopIntegrationVerifier(PackageTest test,
boolean integrated) {
Function<List<String>, String> verifier = (lines) -> {
// Lookup for xdg commands
--- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java Mon Sep 30 19:11:19 2019 -0400
+++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java Mon Sep 30 19:33:13 2019 -0400
@@ -129,8 +129,8 @@
}
public PackageTest addBundleDesktopIntegrationVerifier(boolean integrated) {
- forTypes(LINUX_DEB, () -> {
- LinuxHelper.addDebBundleDesktopIntegrationVerifier(this, integrated);
+ forTypes(LINUX, () -> {
+ LinuxHelper.addBundleDesktopIntegrationVerifier(this, integrated);
});
return this;
}
@@ -322,10 +322,7 @@
private void verifyPackageBundle(JPackageCommand cmd,
Executor.Result result) {
if (PackageType.LINUX.contains(cmd.packageType())) {
- TKit.assertNotEquals(0L, LinuxHelper.getInstalledPackageSizeKB(
- cmd), String.format(
- "Check installed size of [%s] package in KB is not zero",
- LinuxHelper.getPackageName(cmd)));
+ LinuxHelper.verifyPackageBundleEssential(cmd);
}
bundleVerifiers.stream().forEach(v -> v.accept(cmd, result));
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jpackage/junit/jdk/jpackage/internal/ToolValidatorTest.java Mon Sep 30 19:33:13 2019 -0400
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 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.nio.file.Path;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assert.*;
+import org.junit.Test;
+
+
+public class ToolValidatorTest {
+
+ @Test
+ public void testAvailable() {
+ assertNull(new ToolValidator(TOOL_JAVA).validate());
+ }
+
+ @Test
+ public void testNotAvailable() {
+ assertValidationFailure(new ToolValidator(TOOL_UNKNOWN).validate(), true);
+ }
+
+ @Test
+ public void testVersionParserUsage() {
+ // Without minimal version configured, version parser should not be used
+ new ToolValidator(TOOL_JAVA).setVersionParser(unused -> {
+ throw new RuntimeException();
+ }).validate();
+
+ // Minimal version is 1, actual is 10. Should be OK.
+ assertNull(
+ new ToolValidator(TOOL_JAVA).setMinimalVersion(
+ "1").setVersionParser(unused -> "10").validate());
+
+ // Minimal version is 5, actual is 4.99.37. Error expected.
+ assertValidationFailure(new ToolValidator(TOOL_JAVA).setMinimalVersion(
+ "5").setVersionParser(unused -> "4.99.37").validate(), false);
+ }
+
+ private static void assertValidationFailure(ConfigException v,
+ boolean withCause) {
+ assertNotNull(v);
+ assertThat("", is(not(v.getMessage().strip())));
+ assertThat("", is(not(v.advice.strip())));
+ if (withCause) {
+ assertNotNull(v.getCause());
+ } else {
+ assertNull(v.getCause());
+ }
+ }
+
+ private final static String TOOL_JAVA;
+ private final static String TOOL_UNKNOWN = Path.of(System.getProperty(
+ "java.home"), "bin").toString();
+
+ static {
+ String fname = "java";
+ if (Platform.isWindows()) {
+ fname = fname + ".exe";
+ }
+ TOOL_JAVA = Path.of(System.getProperty("java.home"), "bin", fname).toString();
+ }
+}
--- a/test/jdk/tools/jpackage/junit/junit.java Mon Sep 30 19:11:19 2019 -0400
+++ b/test/jdk/tools/jpackage/junit/junit.java Mon Sep 30 19:33:13 2019 -0400
@@ -31,5 +31,6 @@
* jdk.jpackage.internal.PathGroupTest
* jdk.jpackage.internal.DeployParamsTest
* jdk.jpackage.internal.ApplicationLayoutTest
+ * jdk.jpackage.internal.ToolValidatorTest
* jdk.jpackage.internal.AppImageFileTest
*/
--- a/test/jdk/tools/jpackage/linux/PackageDepsTest.java Mon Sep 30 19:11:19 2019 -0400
+++ b/test/jdk/tools/jpackage/linux/PackageDepsTest.java Mon Sep 30 19:33:13 2019 -0400
@@ -24,6 +24,7 @@
import jdk.jpackage.test.TKit;
import jdk.jpackage.test.PackageTest;
import jdk.jpackage.test.PackageType;
+import jdk.jpackage.test.LinuxHelper;
/**
@@ -74,10 +75,14 @@
.addInitializer(cmd -> {
cmd.addArguments("--linux-package-deps", PREREQ_PACKAGE_NAME);
})
- .forTypes(PackageType.LINUX_DEB)
- .addBundlePropertyVerifier("Depends", PREREQ_PACKAGE_NAME)
- .forTypes(PackageType.LINUX_RPM)
- .addBundlePropertyVerifier("Requires", PREREQ_PACKAGE_NAME)
+ .forTypes(PackageType.LINUX)
+ .addBundleVerifier(cmd -> {
+ TKit.assertTrue(
+ LinuxHelper.getPrerequisitePackages(cmd).contains(
+ PREREQ_PACKAGE_NAME), String.format(
+ "Check package depends on [%s] package",
+ PREREQ_PACKAGE_NAME));
+ })
.run();
});
}