# HG changeset patch # User herrick # Date 1569886393 14400 # Node ID 67ffaf3a2b75cf72078da361e83f4d2141971476 # Parent f09bf58c1f17e0846becdf005101af00149a5212 8231280: Linux packages produced by jpackage should have correct dependencies Submitted-by: asemenyuk Reviewed-by: herrick diff -r f09bf58c1f17 -r 67ffaf3a2b75 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LibProvidersLookup.java --- /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 execute(Path root) throws IOException { + // Get the list of files in the root for which to look up for needed shared libraries + List allPackageFiles = Files.walk(root).filter( + Files::isRegularFile).filter(LibProvidersLookup::canDependOnLibs).collect( + Collectors.toList()); + + Collection neededLibs = getNeededLibsForFiles(allPackageFiles); + + // Get the list of unique package names. + List neededPackages = neededLibs.stream().map(libPath -> { + try { + List 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 packageNames = Collections.emptyList(); + return packageNames; + } + }).flatMap(List::stream).sorted().distinct().collect(Collectors.toList()); + + return neededPackages; + } + + private static List getNeededLibsForFile(Path path) throws IOException { + List 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 getNeededLibsForFiles(List paths) { + // Depending on tool used, the set can contain full paths (ldd) or + // only file names (objdump). + Set allLibs = paths.stream().map(path -> { + List 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 excludedNames = paths.stream().map(Path::getFileName).collect( + Collectors.toSet()); + Iterator 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 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}+\\)"); +} diff -r f09bf58c1f17 -r 67ffaf3a2b75 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppBundler.java --- 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 ICON_PNG = new StandardBundlerParam<>( "icon.png", diff -r f09bf58c1f17 -r 67ffaf3a2b75 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxAppImageBuilder.java --- 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"; diff -r f09bf58c1f17 -r 67ffaf3a2b75 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebBundler.java --- 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 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 EMAIL = @@ -126,39 +139,17 @@ try { String licenseFile = LICENSE_FILE.fetchFrom(params); if (licenseFile != null) { - StringBuilder contentBuilder = new StringBuilder(); - try (Stream 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 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 getToolValidators( + Map params) { + return Stream.of(TOOL_DPKG_DEB, TOOL_DPKG, TOOL_FAKEROOT).map( + ToolValidator::new).collect(Collectors.toList()); + } + + @Override protected File buildPackageBundle( Map replacementData, Map 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 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 + // 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 archPackages = new HashSet<>(); + Set 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 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(); } - } diff -r f09bf58c1f17 -r 67ffaf3a2b75 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageBundler.java --- 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 packageName; + private boolean withFindNeededPackages; @Override final public boolean validate(Map 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 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 getListOfNeededPackages( + Map params) throws IOException { + + PlatformPackage thePackage = createMetaPackage(params); + + final List xdgUtilsPackage; + if (desktopIntegration != null) { + xdgUtilsPackage = desktopIntegration.requiredPackages(); + } else { + xdgUtilsPackage = Collections.emptyList(); + } + + final List 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 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 createDefaultReplacementData( Map params) throws IOException { Map 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 params) + abstract protected void initLibProvidersLookup( + Map params, + LibProvidersLookup libProvidersLookup); + + abstract protected List getToolValidators( + Map params); + + abstract protected void doValidate(Map params) throws ConfigException; abstract protected Map 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 prepareForApplication() throws IOException { + List requiredPackages() { + return Stream.of(List.of(this), nestedIntegrations).flatMap( + List::stream).map(DesktopIntegration::requiredPackagesSelf).flatMap( + List::stream).distinct().collect(Collectors.toList()); + } + + Map create() throws IOException { if (iconFile != null) { // Create application icon file. prepareSrcIconFile(); @@ -386,15 +442,12 @@ data.get(DESKTOP_COMMANDS_INSTALL))); List uninstallShellCmds = new ArrayList<>(Arrays.asList( data.get(DESKTOP_COMMANDS_UNINSTALL))); - for (Map params : launchers) { - DesktopIntegration integration = new DesktopIntegration( - thePackage, params); - + for (var integration: nestedIntegrations) { if (!integration.associations.isEmpty()) { needCleanupScripts = true; } - Map launcherData = integration.prepareForApplication(); + Map launcherData = integration.create(); installShellCmds.add(launcherData.get(DESKTOP_COMMANDS_INSTALL)); uninstallShellCmds.add(launcherData.get( @@ -421,6 +474,13 @@ return data; } + private List requiredPackagesSelf() { + if (desktopFile != null) { + return List.of("xdg-utils"); + } + return Collections.emptyList(); + } + private Map createDataForDesktopFile( Map params) { Map data = new HashMap<>(); @@ -558,6 +618,8 @@ private final DesktopFile desktopFile; private final DesktopFile iconFile; + final private List nestedIntegrations; + private final Map desktopFileData; /** @@ -709,4 +771,6 @@ return String.join(System.lineSeparator(), commands.stream().filter( s -> s != null && !s.isEmpty()).collect(Collectors.toList())); } + + private DesktopIntegration desktopIntegration; } diff -r f09bf58c1f17 -r 67ffaf3a2b75 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmBundler.java --- 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 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 getToolValidators( + Map params) { + return List.of(createRpmbuildToolValidator()); } @Override @@ -193,6 +177,18 @@ return data; } + @Override + protected void initLibProvidersLookup( + Map 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 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 diff -r f09bf58c1f17 -r 67ffaf3a2b75 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/LinuxResources.properties --- 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. diff -r f09bf58c1f17 -r 67ffaf3a2b75 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/LinuxResources_ja.properties --- 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. diff -r f09bf58c1f17 -r 67ffaf3a2b75 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/LinuxResources_zh_CN.properties --- 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. diff -r f09bf58c1f17 -r 67ffaf3a2b75 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.control --- 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 diff -r f09bf58c1f17 -r 67ffaf3a2b75 src/jdk.jpackage/linux/classes/jdk/jpackage/internal/resources/template.spec --- 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 diff -r f09bf58c1f17 -r 67ffaf3a2b75 src/jdk.jpackage/share/classes/jdk/jpackage/internal/ConfigException.java --- 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; diff -r f09bf58c1f17 -r 67ffaf3a2b75 src/jdk.jpackage/share/classes/jdk/jpackage/internal/Executor.java --- /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> 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 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 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> 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 output; + private Consumer> outputConsumer; +} diff -r f09bf58c1f17 -r 67ffaf3a2b75 src/jdk.jpackage/share/classes/jdk/jpackage/internal/I18N.java --- /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"); + } + } +} diff -r f09bf58c1f17 -r 67ffaf3a2b75 src/jdk.jpackage/share/classes/jdk/jpackage/internal/IOUtils.java --- 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 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(); } } diff -r f09bf58c1f17 -r 67ffaf3a2b75 src/jdk.jpackage/share/classes/jdk/jpackage/internal/ToolValidator.java --- /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, String> v) { + versionParser = v; + return this; + } + + ConfigException validate() { + List 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 args; + private String minimalVersion; + private Function, String> versionParser; +} diff -r f09bf58c1f17 -r 67ffaf3a2b75 src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties --- 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 diff -r f09bf58c1f17 -r 67ffaf3a2b75 src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources_ja.properties --- 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 diff -r f09bf58c1f17 -r 67ffaf3a2b75 src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources_zh_CN.properties --- 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 diff -r f09bf58c1f17 -r 67ffaf3a2b75 test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java --- 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()); diff -r f09bf58c1f17 -r 67ffaf3a2b75 test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java --- 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 expectedCriticalRuntimePaths = CRITICAL_RUNTIME_FILES.stream().map( + runtimeDir::resolve).collect(Collectors.toSet()); + Set actualCriticalRuntimePaths = getPackageFiles(cmd).filter( + expectedCriticalRuntimePaths::contains).collect( + Collectors.toSet()); + checkPrerequisites = expectedCriticalRuntimePaths.equals( + actualCriticalRuntimePaths); + } else { + checkPrerequisites = true; + } + + List 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 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, String> verifier = (lines) -> { // Lookup for xdg commands diff -r f09bf58c1f17 -r 67ffaf3a2b75 test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java --- 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)); } diff -r f09bf58c1f17 -r 67ffaf3a2b75 test/jdk/tools/jpackage/junit/jdk/jpackage/internal/ToolValidatorTest.java --- /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(); + } +} diff -r f09bf58c1f17 -r 67ffaf3a2b75 test/jdk/tools/jpackage/junit/junit.java --- 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 */ diff -r f09bf58c1f17 -r 67ffaf3a2b75 test/jdk/tools/jpackage/linux/PackageDepsTest.java --- 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(); }); }