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