1 /* 2 * Copyright (c) 2012, 2020, 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. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package jdk.incubator.jpackage.internal; 27 28 import java.io.File; 29 import java.io.IOException; 30 import java.nio.file.FileVisitResult; 31 import java.nio.file.Files; 32 import java.nio.file.Path; 33 import java.nio.file.SimpleFileVisitor; 34 import java.nio.file.attribute.BasicFileAttributes; 35 36 import java.nio.file.attribute.PosixFilePermission; 37 import java.nio.file.attribute.PosixFilePermissions; 38 import java.text.MessageFormat; 39 import java.util.ArrayList; 40 import java.util.HashMap; 41 import java.util.HashSet; 42 import java.util.List; 43 import java.util.Map; 44 import java.util.Set; 45 import java.util.regex.Matcher; 46 import java.util.regex.Pattern; 47 import java.util.stream.Collectors; 48 import java.util.stream.Stream; 49 import static jdk.incubator.jpackage.internal.OverridableResource.createResource; 50 import static jdk.incubator.jpackage.internal.StandardBundlerParam.APP_NAME; 51 import static jdk.incubator.jpackage.internal.StandardBundlerParam.VERSION; 52 import static jdk.incubator.jpackage.internal.StandardBundlerParam.RELEASE; 53 import static jdk.incubator.jpackage.internal.StandardBundlerParam.VENDOR; 54 import static jdk.incubator.jpackage.internal.StandardBundlerParam.LICENSE_FILE; 55 import static jdk.incubator.jpackage.internal.StandardBundlerParam.COPYRIGHT; 56 57 public class LinuxDebBundler extends LinuxPackageBundler { 58 59 // Debian rules for package naming are used here 60 // https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Source 61 // 62 // Package names must consist only of lower case letters (a-z), 63 // digits (0-9), plus (+) and minus (-) signs, and periods (.). 64 // They must be at least two characters long and 65 // must start with an alphanumeric character. 66 // 67 private static final Pattern DEB_PACKAGE_NAME_PATTERN = 68 Pattern.compile("^[a-z][a-z\\d\\+\\-\\.]+"); 69 70 private static final BundlerParamInfo<String> PACKAGE_NAME = 71 new StandardBundlerParam<> ( 72 Arguments.CLIOptions.LINUX_BUNDLE_NAME.getId(), 73 String.class, 74 params -> { 75 String nm = APP_NAME.fetchFrom(params); 76 77 if (nm == null) return null; 78 79 // make sure to lower case and spaces/underscores become dashes 80 nm = nm.toLowerCase().replaceAll("[ _]", "-"); 81 return nm; 82 }, 83 (s, p) -> { 84 if (!DEB_PACKAGE_NAME_PATTERN.matcher(s).matches()) { 85 throw new IllegalArgumentException(new ConfigException( 86 MessageFormat.format(I18N.getString( 87 "error.invalid-value-for-package-name"), s), 88 I18N.getString( 89 "error.invalid-value-for-package-name.advice"))); 90 } 91 92 return s; 93 }); 94 95 private final static String TOOL_DPKG_DEB = "dpkg-deb"; 96 private final static String TOOL_DPKG = "dpkg"; 97 private final static String TOOL_FAKEROOT = "fakeroot"; 98 99 private final static String DEB_ARCH; 100 static { 101 String debArch; 102 try { 103 debArch = Executor.of(TOOL_DPKG, "--print-architecture").saveOutput( 104 true).executeExpectSuccess().getOutput().get(0); 105 } catch (IOException ex) { 106 debArch = null; 107 } 108 DEB_ARCH = debArch; 109 } 110 111 private static final BundlerParamInfo<String> FULL_PACKAGE_NAME = 112 new StandardBundlerParam<>( 113 "linux.deb.fullPackageName", String.class, params -> { 114 return PACKAGE_NAME.fetchFrom(params) 115 + "_" + VERSION.fetchFrom(params) 116 + "-" + RELEASE.fetchFrom(params) 117 + "_" + DEB_ARCH; 118 }, (s, p) -> s); 119 120 private static final BundlerParamInfo<String> EMAIL = 121 new StandardBundlerParam<> ( 122 Arguments.CLIOptions.LINUX_DEB_MAINTAINER.getId(), 123 String.class, 124 params -> "Unknown", 125 (s, p) -> s); 126 127 private static final BundlerParamInfo<String> MAINTAINER = 128 new StandardBundlerParam<> ( 129 Arguments.CLIOptions.LINUX_DEB_MAINTAINER.getId() + ".internal", 130 String.class, 131 params -> VENDOR.fetchFrom(params) + " <" 132 + EMAIL.fetchFrom(params) + ">", 133 (s, p) -> s); 134 135 private static final BundlerParamInfo<String> SECTION = 136 new StandardBundlerParam<>( 137 Arguments.CLIOptions.LINUX_CATEGORY.getId(), 138 String.class, 139 params -> "misc", 140 (s, p) -> s); 141 142 private static final BundlerParamInfo<String> LICENSE_TEXT = 143 new StandardBundlerParam<> ( 144 "linux.deb.licenseText", 145 String.class, 146 params -> { 147 try { 148 String licenseFile = LICENSE_FILE.fetchFrom(params); 149 if (licenseFile != null) { 150 return Files.readString(Path.of(licenseFile)); 151 } 152 } catch (IOException e) { 153 Log.verbose(e); 154 } 155 return "Unknown"; 156 }, 157 (s, p) -> s); 158 159 public LinuxDebBundler() { 160 super(PACKAGE_NAME); 161 } 162 163 @Override 164 public void doValidate(Map<String, ? super Object> params) 165 throws ConfigException { 166 167 // Show warning if license file is missing 168 if (LICENSE_FILE.fetchFrom(params) == null) { 169 Log.verbose(I18N.getString("message.debs-like-licenses")); 170 } 171 } 172 173 @Override 174 protected List<ToolValidator> getToolValidators( 175 Map<String, ? super Object> params) { 176 return Stream.of(TOOL_DPKG_DEB, TOOL_DPKG, TOOL_FAKEROOT).map( 177 ToolValidator::new).collect(Collectors.toList()); 178 } 179 180 @Override 181 protected File buildPackageBundle( 182 Map<String, String> replacementData, 183 Map<String, ? super Object> params, File outputParentDir) throws 184 PackagerException, IOException { 185 186 prepareProjectConfig(replacementData, params); 187 adjustPermissionsRecursive(createMetaPackage(params).sourceRoot().toFile()); 188 return buildDeb(params, outputParentDir); 189 } 190 191 private static final Pattern PACKAGE_NAME_REGEX = Pattern.compile("^(^\\S+):"); 192 193 @Override 194 protected void initLibProvidersLookup( 195 Map<String, ? super Object> params, 196 LibProvidersLookup libProvidersLookup) { 197 198 // 199 // `dpkg -S` command does glob pattern lookup. If not the absolute path 200 // to the file is specified it might return mltiple package names. 201 // Even for full paths multiple package names can be returned as 202 // it is OK for multiple packages to provide the same file. `/opt` 203 // directory is such an example. So we have to deal with multiple 204 // packages per file situation. 205 // 206 // E.g.: `dpkg -S libc.so.6` command reports three packages: 207 // libc6-x32: /libx32/libc.so.6 208 // libc6:amd64: /lib/x86_64-linux-gnu/libc.so.6 209 // libc6-i386: /lib32/libc.so.6 210 // `:amd64` is architecture suffix and can (should) be dropped. 211 // Still need to decide what package to choose from three. 212 // libc6-x32 and libc6-i386 both depend on libc6: 213 // $ dpkg -s libc6-x32 214 // Package: libc6-x32 215 // Status: install ok installed 216 // Priority: optional 217 // Section: libs 218 // Installed-Size: 10840 219 // Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com> 220 // Architecture: amd64 221 // Source: glibc 222 // Version: 2.23-0ubuntu10 223 // Depends: libc6 (= 2.23-0ubuntu10) 224 // 225 // We can dive into tracking dependencies, but this would be overly 226 // complicated. 227 // 228 // For simplicity lets consider the following rules: 229 // 1. If there is one item in `dpkg -S` output, accept it. 230 // 2. If there are multiple items in `dpkg -S` output and there is at 231 // least one item with the default arch suffix (DEB_ARCH), 232 // accept only these items. 233 // 3. If there are multiple items in `dpkg -S` output and there are 234 // no with the default arch suffix (DEB_ARCH), accept all items. 235 // So lets use this heuristics: don't accept packages for whom 236 // `dpkg -p` command fails. 237 // 4. Arch suffix should be stripped from accepted package names. 238 // 239 240 libProvidersLookup.setPackageLookup(file -> { 241 Set<String> archPackages = new HashSet<>(); 242 Set<String> otherPackages = new HashSet<>(); 243 244 Executor.of(TOOL_DPKG, "-S", file.toString()) 245 .saveOutput(true).executeExpectSuccess() 246 .getOutput().forEach(line -> { 247 Matcher matcher = PACKAGE_NAME_REGEX.matcher(line); 248 if (matcher.find()) { 249 String name = matcher.group(1); 250 if (name.endsWith(":" + DEB_ARCH)) { 251 // Strip arch suffix 252 name = name.substring(0, 253 name.length() - (DEB_ARCH.length() + 1)); 254 archPackages.add(name); 255 } else { 256 otherPackages.add(name); 257 } 258 } 259 }); 260 261 if (!archPackages.isEmpty()) { 262 return archPackages.stream(); 263 } 264 return otherPackages.stream(); 265 }); 266 } 267 268 @Override 269 protected List<ConfigException> verifyOutputBundle( 270 Map<String, ? super Object> params, Path packageBundle) { 271 List<ConfigException> errors = new ArrayList<>(); 272 273 String controlFileName = "control"; 274 275 List<PackageProperty> properties = List.of( 276 new PackageProperty("Package", PACKAGE_NAME.fetchFrom(params), 277 "APPLICATION_PACKAGE", controlFileName), 278 new PackageProperty("Version", String.format("%s-%s", 279 VERSION.fetchFrom(params), RELEASE.fetchFrom(params)), 280 "APPLICATION_VERSION-APPLICATION_RELEASE", 281 controlFileName), 282 new PackageProperty("Architecture", DEB_ARCH, "APPLICATION_ARCH", 283 controlFileName)); 284 285 List<String> cmdline = new ArrayList<>(List.of(TOOL_DPKG_DEB, "-f", 286 packageBundle.toString())); 287 properties.forEach(property -> cmdline.add(property.name)); 288 try { 289 Map<String, String> actualValues = Executor.of(cmdline.toArray(String[]::new)) 290 .saveOutput(true) 291 .executeExpectSuccess() 292 .getOutput().stream() 293 .map(line -> line.split(":\\s+", 2)) 294 .collect(Collectors.toMap( 295 components -> components[0], 296 components -> components[1])); 297 properties.forEach(property -> errors.add(property.verifyValue( 298 actualValues.get(property.name)))); 299 } catch (IOException ex) { 300 // Ignore error as it is not critical. Just report it. 301 Log.verbose(ex); 302 } 303 304 return errors; 305 } 306 307 /* 308 * set permissions with a string like "rwxr-xr-x" 309 * 310 * This cannot be directly backport to 22u which is built with 1.6 311 */ 312 private void setPermissions(File file, String permissions) { 313 Set<PosixFilePermission> filePermissions = 314 PosixFilePermissions.fromString(permissions); 315 try { 316 if (file.exists()) { 317 Files.setPosixFilePermissions(file.toPath(), filePermissions); 318 } 319 } catch (IOException ex) { 320 Log.error(ex.getMessage()); 321 Log.verbose(ex); 322 } 323 324 } 325 326 public static boolean isDebian() { 327 // we are just going to run "dpkg -s coreutils" and assume Debian 328 // or deritive if no error is returned. 329 try { 330 Executor.of(TOOL_DPKG, "-s", "coreutils").executeExpectSuccess(); 331 return true; 332 } catch (IOException e) { 333 // just fall thru 334 } 335 return false; 336 } 337 338 private void adjustPermissionsRecursive(File dir) throws IOException { 339 Files.walkFileTree(dir.toPath(), new SimpleFileVisitor<Path>() { 340 @Override 341 public FileVisitResult visitFile(Path file, 342 BasicFileAttributes attrs) 343 throws IOException { 344 if (file.endsWith(".so") || !Files.isExecutable(file)) { 345 setPermissions(file.toFile(), "rw-r--r--"); 346 } else if (Files.isExecutable(file)) { 347 setPermissions(file.toFile(), "rwxr-xr-x"); 348 } 349 return FileVisitResult.CONTINUE; 350 } 351 352 @Override 353 public FileVisitResult postVisitDirectory(Path dir, IOException e) 354 throws IOException { 355 if (e == null) { 356 setPermissions(dir.toFile(), "rwxr-xr-x"); 357 return FileVisitResult.CONTINUE; 358 } else { 359 // directory iteration failed 360 throw e; 361 } 362 } 363 }); 364 } 365 366 private class DebianFile { 367 368 DebianFile(Path dstFilePath, String comment) { 369 this.dstFilePath = dstFilePath; 370 this.comment = comment; 371 } 372 373 DebianFile setExecutable() { 374 permissions = "rwxr-xr-x"; 375 return this; 376 } 377 378 void create(Map<String, String> data, Map<String, ? super Object> params) 379 throws IOException { 380 createResource("template." + dstFilePath.getFileName().toString(), 381 params) 382 .setCategory(I18N.getString(comment)) 383 .setSubstitutionData(data) 384 .saveToFile(dstFilePath); 385 if (permissions != null) { 386 setPermissions(dstFilePath.toFile(), permissions); 387 } 388 } 389 390 private final Path dstFilePath; 391 private final String comment; 392 private String permissions; 393 } 394 395 private void prepareProjectConfig(Map<String, String> data, 396 Map<String, ? super Object> params) throws IOException { 397 398 Path configDir = createMetaPackage(params).sourceRoot().resolve("DEBIAN"); 399 List<DebianFile> debianFiles = new ArrayList<>(); 400 debianFiles.add(new DebianFile( 401 configDir.resolve("control"), 402 "resource.deb-control-file")); 403 debianFiles.add(new DebianFile( 404 configDir.resolve("preinst"), 405 "resource.deb-preinstall-script").setExecutable()); 406 debianFiles.add(new DebianFile( 407 configDir.resolve("prerm"), 408 "resource.deb-prerm-script").setExecutable()); 409 debianFiles.add(new DebianFile( 410 configDir.resolve("postinst"), 411 "resource.deb-postinstall-script").setExecutable()); 412 debianFiles.add(new DebianFile( 413 configDir.resolve("postrm"), 414 "resource.deb-postrm-script").setExecutable()); 415 416 if (!StandardBundlerParam.isRuntimeInstaller(params)) { 417 debianFiles.add(new DebianFile( 418 getConfig_CopyrightFile(params).toPath(), 419 "resource.copyright-file")); 420 } 421 422 for (DebianFile debianFile : debianFiles) { 423 debianFile.create(data, params); 424 } 425 } 426 427 @Override 428 protected Map<String, String> createReplacementData( 429 Map<String, ? super Object> params) throws IOException { 430 Map<String, String> data = new HashMap<>(); 431 432 data.put("APPLICATION_MAINTAINER", MAINTAINER.fetchFrom(params)); 433 data.put("APPLICATION_SECTION", SECTION.fetchFrom(params)); 434 data.put("APPLICATION_COPYRIGHT", COPYRIGHT.fetchFrom(params)); 435 data.put("APPLICATION_LICENSE_TEXT", LICENSE_TEXT.fetchFrom(params)); 436 data.put("APPLICATION_ARCH", DEB_ARCH); 437 data.put("APPLICATION_INSTALLED_SIZE", Long.toString( 438 createMetaPackage(params).sourceApplicationLayout().sizeInBytes() >> 10)); 439 440 return data; 441 } 442 443 private File getConfig_CopyrightFile(Map<String, ? super Object> params) { 444 final String installDir = LINUX_INSTALL_DIR.fetchFrom(params); 445 final String packageName = PACKAGE_NAME.fetchFrom(params); 446 447 final Path installPath; 448 if (isInstallDirInUsrTree(installDir) || installDir.startsWith("/usr/")) { 449 installPath = Path.of("/usr/share/doc/", packageName, "copyright"); 450 } else { 451 installPath = Path.of(installDir, packageName, "share/doc/copyright"); 452 } 453 454 return createMetaPackage(params).sourceRoot().resolve( 455 Path.of("/").relativize(installPath)).toFile(); 456 } 457 458 private File buildDeb(Map<String, ? super Object> params, 459 File outdir) throws IOException { 460 File outFile = new File(outdir, 461 FULL_PACKAGE_NAME.fetchFrom(params)+".deb"); 462 Log.verbose(MessageFormat.format(I18N.getString( 463 "message.outputting-to-location"), outFile.getAbsolutePath())); 464 465 PlatformPackage thePackage = createMetaPackage(params); 466 467 List<String> cmdline = new ArrayList<>(); 468 cmdline.addAll(List.of(TOOL_FAKEROOT, TOOL_DPKG_DEB)); 469 if (Log.isVerbose()) { 470 cmdline.add("--verbose"); 471 } 472 cmdline.addAll(List.of("-b", thePackage.sourceRoot().toString(), 473 outFile.getAbsolutePath())); 474 475 // run dpkg 476 Executor.of(cmdline.toArray(String[]::new)).executeExpectSuccess(); 477 478 Log.verbose(MessageFormat.format(I18N.getString( 479 "message.output-to-location"), outFile.getAbsolutePath())); 480 481 return outFile; 482 } 483 484 @Override 485 public String getName() { 486 return I18N.getString("deb.bundler.name"); 487 } 488 489 @Override 490 public String getID() { 491 return "deb"; 492 } 493 494 @Override 495 public boolean supported(boolean runtimeInstaller) { 496 return Platform.isLinux() && (new ToolValidator(TOOL_DPKG_DEB).validate() == null); 497 } 498 499 @Override 500 public boolean isDefault() { 501 return isDebian(); 502 } 503 }