1 /* 2 * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 package jdk.jpackage.test; 24 25 import java.io.IOException; 26 import java.nio.file.Files; 27 import java.nio.file.Path; 28 import java.util.*; 29 import java.util.function.Function; 30 import java.util.stream.Collectors; 31 import java.util.stream.Stream; 32 import jdk.jpackage.test.PackageTest.PackageHandlers; 33 34 public class LinuxHelper { 35 private static String getRelease(JPackageCommand cmd) { 36 return cmd.getArgumentValue("--linux-app-release", () -> "1"); 37 } 38 39 public static String getPackageName(JPackageCommand cmd) { 40 cmd.verifyIsOfType(PackageType.LINUX); 41 return cmd.getArgumentValue("--linux-package-name", 42 () -> cmd.name().toLowerCase()); 43 } 44 45 public static Path getDesktopFile(JPackageCommand cmd) { 46 return getDesktopFile(cmd, null); 47 } 48 49 public static Path getDesktopFile(JPackageCommand cmd, String launcherName) { 50 cmd.verifyIsOfType(PackageType.LINUX); 51 String desktopFileName = String.format("%s-%s.desktop", getPackageName( 52 cmd), Optional.ofNullable(launcherName).orElseGet( 53 () -> cmd.name())); 54 return cmd.appLayout().destktopIntegrationDirectory().resolve( 55 desktopFileName); 56 } 57 58 static String getBundleName(JPackageCommand cmd) { 59 cmd.verifyIsOfType(PackageType.LINUX); 60 61 final PackageType packageType = cmd.packageType(); 62 String format = null; 63 switch (packageType) { 64 case LINUX_DEB: 65 format = "%s_%s-%s_%s"; 66 break; 67 68 case LINUX_RPM: 69 format = "%s-%s-%s.%s"; 70 break; 71 } 72 73 final String release = getRelease(cmd); 74 final String version = cmd.version(); 75 76 return String.format(format, getPackageName(cmd), version, release, 77 getDefaultPackageArch(packageType)) + packageType.getSuffix(); 78 } 79 80 public static Stream<Path> getPackageFiles(JPackageCommand cmd) { 81 cmd.verifyIsOfType(PackageType.LINUX); 82 83 final PackageType packageType = cmd.packageType(); 84 final Path packageFile = cmd.outputBundle(); 85 86 Executor exec = null; 87 switch (packageType) { 88 case LINUX_DEB: 89 exec = Executor.of("dpkg", "--contents").addArgument(packageFile); 90 break; 91 92 case LINUX_RPM: 93 exec = Executor.of("rpm", "-qpl").addArgument(packageFile); 94 break; 95 } 96 97 Stream<String> lines = exec.executeAndGetOutput().stream(); 98 if (packageType == PackageType.LINUX_DEB) { 99 // Typical text lines produced by dpkg look like: 100 // drwxr-xr-x root/root 0 2019-08-30 05:30 ./opt/appcategorytest/runtime/lib/ 101 // -rw-r--r-- root/root 574912 2019-08-30 05:30 ./opt/appcategorytest/runtime/lib/libmlib_image.so 102 // Need to skip all fields but absolute path to file. 103 lines = lines.map(line -> line.substring(line.indexOf(" ./") + 2)); 104 } 105 return lines.map(Path::of); 106 } 107 108 public static List<String> getPrerequisitePackages(JPackageCommand cmd) { 109 cmd.verifyIsOfType(PackageType.LINUX); 110 var packageType = cmd.packageType(); 111 switch (packageType) { 112 case LINUX_DEB: 113 return Stream.of(getDebBundleProperty(cmd.outputBundle(), 114 "Depends").split(",")).map(String::strip).collect( 115 Collectors.toList()); 116 117 case LINUX_RPM: 118 return Executor.of("rpm", "-qp", "-R") 119 .addArgument(cmd.outputBundle()) 120 .executeAndGetOutput(); 121 } 122 // Unreachable 123 return null; 124 } 125 126 public static String getBundleProperty(JPackageCommand cmd, 127 String propertyName) { 128 return getBundleProperty(cmd, 129 Map.of(PackageType.LINUX_DEB, propertyName, 130 PackageType.LINUX_RPM, propertyName)); 131 } 132 133 public static String getBundleProperty(JPackageCommand cmd, 134 Map<PackageType, String> propertyName) { 135 cmd.verifyIsOfType(PackageType.LINUX); 136 var packageType = cmd.packageType(); 137 switch (packageType) { 138 case LINUX_DEB: 139 return getDebBundleProperty(cmd.outputBundle(), propertyName.get( 140 packageType)); 141 142 case LINUX_RPM: 143 return getRpmBundleProperty(cmd.outputBundle(), propertyName.get( 144 packageType)); 145 } 146 // Unrechable 147 return null; 148 } 149 150 static PackageHandlers createDebPackageHandlers() { 151 PackageHandlers deb = new PackageHandlers(); 152 deb.installHandler = cmd -> { 153 cmd.verifyIsOfType(PackageType.LINUX_DEB); 154 Executor.of("sudo", "dpkg", "-i") 155 .addArgument(cmd.outputBundle()) 156 .execute(); 157 }; 158 deb.uninstallHandler = cmd -> { 159 cmd.verifyIsOfType(PackageType.LINUX_DEB); 160 Executor.of("sudo", "dpkg", "-r", getPackageName(cmd)).execute(); 161 }; 162 deb.unpackHandler = (cmd, destinationDir) -> { 163 cmd.verifyIsOfType(PackageType.LINUX_DEB); 164 Executor.of("dpkg", "-x") 165 .addArgument(cmd.outputBundle()) 166 .addArgument(destinationDir) 167 .execute(); 168 return destinationDir.resolve(String.format(".%s", 169 cmd.appInstallationDirectory())).normalize(); 170 }; 171 return deb; 172 } 173 174 static PackageHandlers createRpmPackageHandlers() { 175 PackageHandlers rpm = new PackageHandlers(); 176 rpm.installHandler = cmd -> { 177 cmd.verifyIsOfType(PackageType.LINUX_RPM); 178 Executor.of("sudo", "rpm", "-i") 179 .addArgument(cmd.outputBundle()) 180 .execute(); 181 }; 182 rpm.uninstallHandler = cmd -> { 183 cmd.verifyIsOfType(PackageType.LINUX_RPM); 184 Executor.of("sudo", "rpm", "-e", getPackageName(cmd)).execute(); 185 }; 186 rpm.unpackHandler = (cmd, destinationDir) -> { 187 cmd.verifyIsOfType(PackageType.LINUX_RPM); 188 Executor.of("sh", "-c", String.format( 189 "rpm2cpio '%s' | cpio -idm --quiet", 190 JPackageCommand.escapeAndJoin( 191 cmd.outputBundle().toAbsolutePath().toString()))) 192 .setDirectory(destinationDir) 193 .execute(); 194 return destinationDir.resolve(String.format(".%s", 195 cmd.appInstallationDirectory())).normalize(); 196 }; 197 198 return rpm; 199 } 200 201 static Path getLauncherPath(JPackageCommand cmd) { 202 cmd.verifyIsOfType(PackageType.LINUX); 203 204 final String launcherName = cmd.name(); 205 final String launcherRelativePath = Path.of("/bin", launcherName).toString(); 206 207 return getPackageFiles(cmd).filter(path -> path.toString().endsWith( 208 launcherRelativePath)).findFirst().or(() -> { 209 TKit.assertUnexpected(String.format( 210 "Failed to find %s in %s package", launcherName, 211 getPackageName(cmd))); 212 return null; 213 }).get(); 214 } 215 216 static long getInstalledPackageSizeKB(JPackageCommand cmd) { 217 cmd.verifyIsOfType(PackageType.LINUX); 218 219 final Path packageFile = cmd.outputBundle(); 220 switch (cmd.packageType()) { 221 case LINUX_DEB: 222 return Long.parseLong(getDebBundleProperty(packageFile, 223 "Installed-Size")); 224 225 case LINUX_RPM: 226 return Long.parseLong(getRpmBundleProperty(packageFile, "Size")) >> 10; 227 } 228 229 return 0; 230 } 231 232 static String getDebBundleProperty(Path bundle, String fieldName) { 233 return Executor.of("dpkg-deb", "-f") 234 .addArgument(bundle) 235 .addArgument(fieldName) 236 .executeAndGetFirstLineOfOutput(); 237 } 238 239 static String getRpmBundleProperty(Path bundle, String fieldName) { 240 return Executor.of("rpm", "-qp", "--queryformat", String.format("%%{%s}", fieldName)) 241 .addArgument(bundle) 242 .executeAndGetFirstLineOfOutput(); 243 } 244 245 static void verifyPackageBundleEssential(JPackageCommand cmd) { 246 String packageName = LinuxHelper.getPackageName(cmd); 247 TKit.assertNotEquals(0L, LinuxHelper.getInstalledPackageSizeKB( 248 cmd), String.format( 249 "Check installed size of [%s] package in KB is not zero", 250 packageName)); 251 252 final boolean checkPrerequisites; 253 if (cmd.isRuntime()) { 254 Path runtimeDir = cmd.appRuntimeDirectory(); 255 Set<Path> expectedCriticalRuntimePaths = CRITICAL_RUNTIME_FILES.stream().map( 256 runtimeDir::resolve).collect(Collectors.toSet()); 257 Set<Path> actualCriticalRuntimePaths = getPackageFiles(cmd).filter( 258 expectedCriticalRuntimePaths::contains).collect( 259 Collectors.toSet()); 260 checkPrerequisites = expectedCriticalRuntimePaths.equals( 261 actualCriticalRuntimePaths); 262 } else { 263 checkPrerequisites = true; 264 } 265 266 List<String> prerequisites = LinuxHelper.getPrerequisitePackages(cmd); 267 if (checkPrerequisites) { 268 final String vitalPackage = "libc"; 269 TKit.assertTrue(prerequisites.stream().filter( 270 dep -> dep.contains(vitalPackage)).findAny().isPresent(), 271 String.format( 272 "Check [%s] package is in the list of required packages %s of [%s] package", 273 vitalPackage, prerequisites, packageName)); 274 } else { 275 TKit.trace(String.format( 276 "Not cheking %s required packages of [%s] package", 277 prerequisites, packageName)); 278 } 279 } 280 281 static void addBundleDesktopIntegrationVerifier(PackageTest test, 282 boolean integrated) { 283 final String xdgUtils = "xdg-utils"; 284 285 test.addBundleVerifier(cmd -> { 286 List<String> prerequisites = getPrerequisitePackages(cmd); 287 boolean xdgUtilsFound = prerequisites.contains(xdgUtils); 288 if (integrated) { 289 TKit.assertTrue(xdgUtilsFound, String.format( 290 "Check [%s] is in the list of required packages %s", 291 xdgUtils, prerequisites)); 292 } else { 293 TKit.assertFalse(xdgUtilsFound, String.format( 294 "Check [%s] is NOT in the list of required packages %s", 295 xdgUtils, prerequisites)); 296 } 297 }); 298 299 test.forTypes(PackageType.LINUX_DEB, () -> { 300 addDebBundleDesktopIntegrationVerifier(test, integrated); 301 }); 302 } 303 304 private static void addDebBundleDesktopIntegrationVerifier(PackageTest test, 305 boolean integrated) { 306 Function<List<String>, String> verifier = (lines) -> { 307 // Lookup for xdg commands 308 return lines.stream().filter(line -> { 309 Set<String> words = Stream.of(line.split("\\s+")).collect( 310 Collectors.toSet()); 311 return words.contains("xdg-desktop-menu") || words.contains( 312 "xdg-mime") || words.contains("xdg-icon-resource"); 313 }).findFirst().orElse(null); 314 }; 315 316 test.addBundleVerifier(cmd -> { 317 TKit.withTempDirectory("dpkg-control-files", tempDir -> { 318 // Extract control Debian package files into temporary directory 319 Executor.of("dpkg", "-e") 320 .addArgument(cmd.outputBundle()) 321 .addArgument(tempDir) 322 .execute(); 323 324 Path controlFile = Path.of("postinst"); 325 326 // Lookup for xdg commands in postinstall script 327 String lineWithXsdCommand = verifier.apply( 328 Files.readAllLines(tempDir.resolve(controlFile))); 329 String assertMsg = String.format( 330 "Check if %s@%s control file uses xdg commands", 331 cmd.outputBundle(), controlFile); 332 if (integrated) { 333 TKit.assertNotNull(lineWithXsdCommand, assertMsg); 334 } else { 335 TKit.assertNull(lineWithXsdCommand, assertMsg); 336 } 337 }); 338 }); 339 } 340 341 static void initFileAssociationsTestFile(Path testFile) { 342 try { 343 // Write something in test file. 344 // On Ubuntu and Oracle Linux empty files are considered 345 // plain text. Seems like a system bug. 346 // 347 // $ >foo.jptest1 348 // $ xdg-mime query filetype foo.jptest1 349 // text/plain 350 // $ echo > foo.jptest1 351 // $ xdg-mime query filetype foo.jptest1 352 // application/x-jpackage-jptest1 353 // 354 Files.write(testFile, Arrays.asList("")); 355 } catch (IOException ex) { 356 throw new RuntimeException(ex); 357 } 358 } 359 360 private static Path getSystemDesktopFilesFolder() { 361 return Stream.of("/usr/share/applications", 362 "/usr/local/share/applications").map(Path::of).filter(dir -> { 363 return Files.exists(dir.resolve("defaults.list")); 364 }).findFirst().orElseThrow(() -> new RuntimeException( 365 "Failed to locate system .desktop files folder")); 366 } 367 368 static void addFileAssociationsVerifier(PackageTest test, FileAssociations fa) { 369 test.addInstallVerifier(cmd -> { 370 if (cmd.isPackageUnpacked("Not running file associations checks")) { 371 return; 372 } 373 374 PackageTest.withTestFileAssociationsFile(fa, testFile -> { 375 String mimeType = queryFileMimeType(testFile); 376 377 TKit.assertEquals(fa.getMime(), mimeType, String.format( 378 "Check mime type of [%s] file", testFile)); 379 380 String desktopFileName = queryMimeTypeDefaultHandler(mimeType); 381 382 Path desktopFile = getSystemDesktopFilesFolder().resolve( 383 desktopFileName); 384 385 TKit.assertFileExists(desktopFile); 386 387 TKit.trace(String.format("Reading [%s] file...", desktopFile)); 388 String mimeHandler = Files.readAllLines(desktopFile).stream().peek( 389 v -> TKit.trace(v)).filter( 390 v -> v.startsWith("Exec=")).map( 391 v -> v.split("=", 2)[1]).findFirst().orElseThrow(); 392 393 TKit.trace(String.format("Done")); 394 395 TKit.assertEquals(cmd.appLauncherPath().toString(), 396 mimeHandler, String.format( 397 "Check mime type handler is the main application launcher")); 398 399 }); 400 }); 401 402 test.addUninstallVerifier(cmd -> { 403 PackageTest.withTestFileAssociationsFile(fa, testFile -> { 404 String mimeType = queryFileMimeType(testFile); 405 406 TKit.assertNotEquals(fa.getMime(), mimeType, String.format( 407 "Check mime type of [%s] file", testFile)); 408 409 String desktopFileName = queryMimeTypeDefaultHandler(fa.getMime()); 410 411 TKit.assertNull(desktopFileName, String.format( 412 "Check there is no default handler for [%s] mime type", 413 fa.getMime())); 414 }); 415 }); 416 } 417 418 private static String queryFileMimeType(Path file) { 419 return Executor.of("xdg-mime", "query", "filetype").addArgument(file) 420 .executeAndGetFirstLineOfOutput(); 421 } 422 423 private static String queryMimeTypeDefaultHandler(String mimeType) { 424 return Executor.of("xdg-mime", "query", "default", mimeType) 425 .executeAndGetFirstLineOfOutput(); 426 } 427 428 public static String getDefaultPackageArch(PackageType type) { 429 if (archs == null) { 430 archs = new HashMap<>(); 431 } 432 433 String arch = archs.get(type); 434 if (arch == null) { 435 Executor exec = null; 436 switch (type) { 437 case LINUX_DEB: 438 exec = Executor.of("dpkg", "--print-architecture"); 439 break; 440 441 case LINUX_RPM: 442 exec = Executor.of("rpmbuild", "--eval=%{_target_cpu}"); 443 break; 444 } 445 arch = exec.executeAndGetFirstLineOfOutput(); 446 archs.put(type, arch); 447 } 448 return arch; 449 } 450 451 static final Set<Path> CRITICAL_RUNTIME_FILES = Set.of(Path.of( 452 "lib/server/libjvm.so")); 453 454 static private Map<PackageType, String> archs; 455 }