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.Arrays; 29 import java.util.HashMap; 30 import java.util.List; 31 import java.util.Map; 32 import java.util.Set; 33 import java.util.function.Function; 34 import java.util.stream.Collectors; 35 import java.util.stream.Stream; 36 37 public class LinuxHelper { 38 private static String getRelease(JPackageCommand cmd) { 39 return cmd.getArgumentValue("--linux-app-release", () -> "1"); 40 } 41 42 public static String getPackageName(JPackageCommand cmd) { 43 cmd.verifyIsOfType(PackageType.LINUX); 44 return cmd.getArgumentValue("--linux-package-name", 45 () -> cmd.name().toLowerCase()); 46 } 47 48 static String getBundleName(JPackageCommand cmd) { 49 cmd.verifyIsOfType(PackageType.LINUX); 50 51 final PackageType packageType = cmd.packageType(); 52 String format = null; 53 switch (packageType) { 54 case LINUX_DEB: 55 format = "%s_%s-%s_%s"; 56 break; 57 58 case LINUX_RPM: 59 format = "%s-%s-%s.%s"; 60 break; 61 } 62 63 final String release = getRelease(cmd); 64 final String version = cmd.version(); 65 66 return String.format(format, getPackageName(cmd), version, release, 67 getDefaultPackageArch(packageType)) + packageType.getSuffix(); 68 } 69 70 public static Stream<Path> getPackageFiles(JPackageCommand cmd) { 71 cmd.verifyIsOfType(PackageType.LINUX); 72 73 final PackageType packageType = cmd.packageType(); 74 final Path packageFile = cmd.outputBundle(); 75 76 Executor exec = new Executor(); 77 switch (packageType) { 78 case LINUX_DEB: 79 exec.setExecutable("dpkg") 80 .addArgument("--contents") 81 .addArgument(packageFile); 82 break; 83 84 case LINUX_RPM: 85 exec.setExecutable("rpm") 86 .addArgument("-qpl") 87 .addArgument(packageFile); 88 break; 89 } 90 91 Stream<String> lines = exec.executeAndGetOutput().stream(); 92 if (packageType == PackageType.LINUX_DEB) { 93 // Typical text lines produced by dpkg look like: 94 // drwxr-xr-x root/root 0 2019-08-30 05:30 ./opt/appcategorytest/runtime/lib/ 95 // -rw-r--r-- root/root 574912 2019-08-30 05:30 ./opt/appcategorytest/runtime/lib/libmlib_image.so 96 // Need to skip all fields but absolute path to file. 97 lines = lines.map(line -> line.substring(line.indexOf(" ./") + 2)); 98 } 99 return lines.map(Path::of); 100 } 101 102 public static List<String> getPrerequisitePackages(JPackageCommand cmd) { 103 cmd.verifyIsOfType(PackageType.LINUX); 104 var packageType = cmd.packageType(); 105 switch (packageType) { 106 case LINUX_DEB: 107 return Stream.of(getDebBundleProperty(cmd.outputBundle(), 108 "Depends").split(",")).map(String::strip).collect( 109 Collectors.toList()); 110 111 case LINUX_RPM: 112 return new Executor().setExecutable("rpm") 113 .addArguments("-qp", "-R", cmd.outputBundle().toString()) 114 .executeAndGetOutput(); 115 } 116 // Unreachable 117 return null; 118 } 119 120 public static String getBundleProperty(JPackageCommand cmd, 121 String propertyName) { 122 return getBundleProperty(cmd, 123 Map.of(PackageType.LINUX_DEB, propertyName, 124 PackageType.LINUX_RPM, propertyName)); 125 } 126 127 public static String getBundleProperty(JPackageCommand cmd, 128 Map<PackageType, String> propertyName) { 129 cmd.verifyIsOfType(PackageType.LINUX); 130 var packageType = cmd.packageType(); 131 switch (packageType) { 132 case LINUX_DEB: 133 return getDebBundleProperty(cmd.outputBundle(), propertyName.get( 134 packageType)); 135 136 case LINUX_RPM: 137 return getRpmBundleProperty(cmd.outputBundle(), propertyName.get( 138 packageType)); 139 } 140 // Unrechable 141 return null; 142 } 143 144 static Path getLauncherPath(JPackageCommand cmd) { 145 cmd.verifyIsOfType(PackageType.LINUX); 146 147 final String launcherName = cmd.name(); 148 final String launcherRelativePath = Path.of("/bin", launcherName).toString(); 149 150 return getPackageFiles(cmd).filter(path -> path.toString().endsWith( 151 launcherRelativePath)).findFirst().or(() -> { 152 TKit.assertUnexpected(String.format( 153 "Failed to find %s in %s package", launcherName, 154 getPackageName(cmd))); 155 return null; 156 }).get(); 157 } 158 159 static long getInstalledPackageSizeKB(JPackageCommand cmd) { 160 cmd.verifyIsOfType(PackageType.LINUX); 161 162 final Path packageFile = cmd.outputBundle(); 163 switch (cmd.packageType()) { 164 case LINUX_DEB: 165 return Long.parseLong(getDebBundleProperty(packageFile, 166 "Installed-Size")); 167 168 case LINUX_RPM: 169 return Long.parseLong(getRpmBundleProperty(packageFile, "Size")) >> 10; 170 } 171 172 return 0; 173 } 174 175 static String getDebBundleProperty(Path bundle, String fieldName) { 176 return new Executor() 177 .setExecutable("dpkg-deb") 178 .addArguments("-f", bundle.toString(), fieldName) 179 .executeAndGetFirstLineOfOutput(); 180 } 181 182 static String getRpmBundleProperty(Path bundle, String fieldName) { 183 return new Executor() 184 .setExecutable("rpm") 185 .addArguments( 186 "-qp", 187 "--queryformat", 188 String.format("%%{%s}", fieldName), 189 bundle.toString()) 190 .executeAndGetFirstLineOfOutput(); 191 } 192 193 static void verifyPackageBundleEssential(JPackageCommand cmd) { 194 String packageName = LinuxHelper.getPackageName(cmd); 195 TKit.assertNotEquals(0L, LinuxHelper.getInstalledPackageSizeKB( 196 cmd), String.format( 197 "Check installed size of [%s] package in KB is not zero", 198 packageName)); 199 200 final boolean checkPrerequisites; 201 if (cmd.isRuntime()) { 202 Path runtimeDir = cmd.appRuntimeDirectory(); 203 Set<Path> expectedCriticalRuntimePaths = CRITICAL_RUNTIME_FILES.stream().map( 204 runtimeDir::resolve).collect(Collectors.toSet()); 205 Set<Path> actualCriticalRuntimePaths = getPackageFiles(cmd).filter( 206 expectedCriticalRuntimePaths::contains).collect( 207 Collectors.toSet()); 208 checkPrerequisites = expectedCriticalRuntimePaths.equals( 209 actualCriticalRuntimePaths); 210 } else { 211 checkPrerequisites = true; 212 } 213 214 List<String> prerequisites = LinuxHelper.getPrerequisitePackages(cmd); 215 if (checkPrerequisites) { 216 final String vitalPackage = "libc"; 217 TKit.assertTrue(prerequisites.stream().filter( 218 dep -> dep.contains(vitalPackage)).findAny().isPresent(), 219 String.format( 220 "Check [%s] package is in the list of required packages %s of [%s] package", 221 vitalPackage, prerequisites, packageName)); 222 } else { 223 TKit.trace(String.format( 224 "Not cheking %s required packages of [%s] package", 225 prerequisites, packageName)); 226 } 227 } 228 229 static void addBundleDesktopIntegrationVerifier(PackageTest test, 230 boolean integrated) { 231 final String xdgUtils = "xdg-utils"; 232 233 test.addBundleVerifier(cmd -> { 234 List<String> prerequisites = getPrerequisitePackages(cmd); 235 boolean xdgUtilsFound = prerequisites.contains(xdgUtils); 236 if (integrated) { 237 TKit.assertTrue(xdgUtilsFound, String.format( 238 "Check [%s] is in the list of required packages %s", 239 xdgUtils, prerequisites)); 240 } else { 241 TKit.assertFalse(xdgUtilsFound, String.format( 242 "Check [%s] is NOT in the list of required packages %s", 243 xdgUtils, prerequisites)); 244 } 245 }); 246 247 test.forTypes(PackageType.LINUX_DEB, () -> { 248 addDebBundleDesktopIntegrationVerifier(test, integrated); 249 }); 250 } 251 252 private static void addDebBundleDesktopIntegrationVerifier(PackageTest test, 253 boolean integrated) { 254 Function<List<String>, String> verifier = (lines) -> { 255 // Lookup for xdg commands 256 return lines.stream().filter(line -> { 257 Set<String> words = Stream.of(line.split("\\s+")).collect( 258 Collectors.toSet()); 259 return words.contains("xdg-desktop-menu") || words.contains( 260 "xdg-mime") || words.contains("xdg-icon-resource"); 261 }).findFirst().orElse(null); 262 }; 263 264 test.addBundleVerifier(cmd -> { 265 TKit.withTempDirectory("dpkg-control-files", tempDir -> { 266 // Extract control Debian package files into temporary directory 267 new Executor() 268 .setExecutable("dpkg") 269 .addArguments( 270 "-e", 271 cmd.outputBundle().toString(), 272 tempDir.toString() 273 ).execute().assertExitCodeIsZero(); 274 275 Path controlFile = Path.of("postinst"); 276 277 // Lookup for xdg commands in postinstall script 278 String lineWithXsdCommand = verifier.apply( 279 Files.readAllLines(tempDir.resolve(controlFile))); 280 String assertMsg = String.format( 281 "Check if %s@%s control file uses xdg commands", 282 cmd.outputBundle(), controlFile); 283 if (integrated) { 284 TKit.assertNotNull(lineWithXsdCommand, assertMsg); 285 } else { 286 TKit.assertNull(lineWithXsdCommand, assertMsg); 287 } 288 }); 289 }); 290 } 291 292 static void initFileAssociationsTestFile(Path testFile) { 293 try { 294 // Write something in test file. 295 // On Ubuntu and Oracle Linux empty files are considered 296 // plain text. Seems like a system bug. 297 // 298 // $ >foo.jptest1 299 // $ xdg-mime query filetype foo.jptest1 300 // text/plain 301 // $ echo > foo.jptest1 302 // $ xdg-mime query filetype foo.jptest1 303 // application/x-jpackage-jptest1 304 // 305 Files.write(testFile, Arrays.asList("")); 306 } catch (IOException ex) { 307 throw new RuntimeException(ex); 308 } 309 } 310 311 private static Path getSystemDesktopFilesFolder() { 312 return Stream.of("/usr/share/applications", 313 "/usr/local/share/applications").map(Path::of).filter(dir -> { 314 return Files.exists(dir.resolve("defaults.list")); 315 }).findFirst().orElseThrow(() -> new RuntimeException( 316 "Failed to locate system .desktop files folder")); 317 } 318 319 static void addFileAssociationsVerifier(PackageTest test, FileAssociations fa) { 320 test.addInstallVerifier(cmd -> { 321 PackageTest.withTestFileAssociationsFile(fa, testFile -> { 322 String mimeType = queryFileMimeType(testFile); 323 324 TKit.assertEquals(fa.getMime(), mimeType, String.format( 325 "Check mime type of [%s] file", testFile)); 326 327 String desktopFileName = queryMimeTypeDefaultHandler(mimeType); 328 329 Path desktopFile = getSystemDesktopFilesFolder().resolve( 330 desktopFileName); 331 332 TKit.assertFileExists(desktopFile); 333 334 TKit.trace(String.format("Reading [%s] file...", desktopFile)); 335 String mimeHandler = Files.readAllLines(desktopFile).stream().peek( 336 v -> TKit.trace(v)).filter( 337 v -> v.startsWith("Exec=")).map( 338 v -> v.split("=", 2)[1]).findFirst().orElseThrow(); 339 340 TKit.trace(String.format("Done")); 341 342 TKit.assertEquals(cmd.appLauncherPath().toString(), 343 mimeHandler, String.format( 344 "Check mime type handler is the main application launcher")); 345 346 }); 347 }); 348 349 test.addUninstallVerifier(cmd -> { 350 PackageTest.withTestFileAssociationsFile(fa, testFile -> { 351 String mimeType = queryFileMimeType(testFile); 352 353 TKit.assertNotEquals(fa.getMime(), mimeType, String.format( 354 "Check mime type of [%s] file", testFile)); 355 356 String desktopFileName = queryMimeTypeDefaultHandler(fa.getMime()); 357 358 TKit.assertNull(desktopFileName, String.format( 359 "Check there is no default handler for [%s] mime type", 360 fa.getMime())); 361 }); 362 }); 363 } 364 365 private static String queryFileMimeType(Path file) { 366 return new Executor() 367 .setExecutable("xdg-mime") 368 .addArguments("query", "filetype", file.toString()) 369 .executeAndGetFirstLineOfOutput(); 370 } 371 372 private static String queryMimeTypeDefaultHandler(String mimeType) { 373 return new Executor() 374 .setExecutable("xdg-mime") 375 .addArguments("query", "default", mimeType) 376 .executeAndGetFirstLineOfOutput(); 377 } 378 379 public static String getDefaultPackageArch(PackageType type) { 380 if (archs == null) { 381 archs = new HashMap<>(); 382 } 383 384 String arch = archs.get(type); 385 if (arch == null) { 386 Executor exec = new Executor(); 387 switch (type) { 388 case LINUX_DEB: 389 exec.setExecutable("dpkg").addArgument( 390 "--print-architecture"); 391 break; 392 393 case LINUX_RPM: 394 exec.setExecutable("rpmbuild").addArgument( 395 "--eval=%{_target_cpu}"); 396 break; 397 } 398 arch = exec.executeAndGetFirstLineOfOutput(); 399 archs.put(type, arch); 400 } 401 return arch; 402 } 403 404 static final Set<Path> CRITICAL_RUNTIME_FILES = Set.of(Path.of( 405 "lib/server/libjvm.so")); 406 407 static private Map<PackageType, String> archs; 408 }