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.jpackage.internal; 27 28 import java.io.*; 29 import java.nio.charset.StandardCharsets; 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.*; 40 import java.util.regex.Pattern; 41 import java.util.stream.Stream; 42 import static jdk.jpackage.internal.LinuxAppBundler.LINUX_INSTALL_DIR; 43 44 import static jdk.jpackage.internal.StandardBundlerParam.*; 45 import static jdk.jpackage.internal.LinuxPackageBundler.I18N; 46 47 public class LinuxDebBundler extends LinuxPackageBundler { 48 49 // Debian rules for package naming are used here 50 // https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Source 51 // 52 // Package names must consist only of lower case letters (a-z), 53 // digits (0-9), plus (+) and minus (-) signs, and periods (.). 54 // They must be at least two characters long and 55 // must start with an alphanumeric character. 56 // 57 private static final Pattern DEB_PACKAGE_NAME_PATTERN = 58 Pattern.compile("^[a-z][a-z\\d\\+\\-\\.]+"); 59 60 private static final BundlerParamInfo<String> PACKAGE_NAME = 61 new StandardBundlerParam<> ( 62 Arguments.CLIOptions.LINUX_BUNDLE_NAME.getId(), 63 String.class, 64 params -> { 65 String nm = APP_NAME.fetchFrom(params); 66 67 if (nm == null) return null; 68 69 // make sure to lower case and spaces/underscores become dashes 70 nm = nm.toLowerCase().replaceAll("[ _]", "-"); 71 return nm; 72 }, 73 (s, p) -> { 74 if (!DEB_PACKAGE_NAME_PATTERN.matcher(s).matches()) { 75 throw new IllegalArgumentException(new ConfigException( 76 MessageFormat.format(I18N.getString( 77 "error.invalid-value-for-package-name"), s), 78 I18N.getString( 79 "error.invalid-value-for-package-name.advice"))); 80 } 81 82 return s; 83 }); 84 85 private static final BundlerParamInfo<String> FULL_PACKAGE_NAME = 86 new StandardBundlerParam<>( 87 "linux.deb.fullPackageName", String.class, params -> { 88 try { 89 return PACKAGE_NAME.fetchFrom(params) 90 + "_" + VERSION.fetchFrom(params) 91 + "-" + RELEASE.fetchFrom(params) 92 + "_" + getDebArch(); 93 } catch (IOException ex) { 94 Log.verbose(ex); 95 return null; 96 } 97 }, (s, p) -> s); 98 99 private static final BundlerParamInfo<String> EMAIL = 100 new StandardBundlerParam<> ( 101 Arguments.CLIOptions.LINUX_DEB_MAINTAINER.getId(), 102 String.class, 103 params -> "Unknown", 104 (s, p) -> s); 105 106 private static final BundlerParamInfo<String> MAINTAINER = 107 new StandardBundlerParam<> ( 108 BundleParams.PARAM_MAINTAINER, 109 String.class, 110 params -> VENDOR.fetchFrom(params) + " <" 111 + EMAIL.fetchFrom(params) + ">", 112 (s, p) -> s); 113 114 private static final BundlerParamInfo<String> SECTION = 115 new StandardBundlerParam<>( 116 Arguments.CLIOptions.LINUX_CATEGORY.getId(), 117 String.class, 118 params -> "misc", 119 (s, p) -> s); 120 121 private static final BundlerParamInfo<String> LICENSE_TEXT = 122 new StandardBundlerParam<> ( 123 "linux.deb.licenseText", 124 String.class, 125 params -> { 126 try { 127 String licenseFile = LICENSE_FILE.fetchFrom(params); 128 if (licenseFile != null) { 129 StringBuilder contentBuilder = new StringBuilder(); 130 try (Stream<String> stream = Files.lines(Path.of( 131 licenseFile), StandardCharsets.UTF_8)) { 132 stream.forEach(s -> contentBuilder.append(s).append( 133 "\n")); 134 } 135 return contentBuilder.toString(); 136 } 137 } catch (Exception e) { 138 Log.verbose(e); 139 } 140 return "Unknown"; 141 }, 142 (s, p) -> s); 143 144 private static final BundlerParamInfo<String> COPYRIGHT_FILE = 145 new StandardBundlerParam<>( 146 Arguments.CLIOptions.LINUX_DEB_COPYRIGHT_FILE.getId(), 147 String.class, 148 params -> null, 149 (s, p) -> s); 150 151 private final static String TOOL_DPKG_DEB = "dpkg-deb"; 152 private final static String TOOL_DPKG = "dpkg"; 153 154 public static boolean testTool(String toolName, String minVersion) { 155 try { 156 ProcessBuilder pb = new ProcessBuilder( 157 toolName, 158 "--version"); 159 // not interested in the output 160 IOUtils.exec(pb, true, null); 161 } catch (Exception e) { 162 Log.verbose(MessageFormat.format(I18N.getString( 163 "message.test-for-tool"), toolName, e.getMessage())); 164 return false; 165 } 166 return true; 167 } 168 169 public LinuxDebBundler() { 170 super(PACKAGE_NAME); 171 } 172 173 @Override 174 public void doValidate(Map<String, ? super Object> params) 175 throws ConfigException { 176 // NOTE: Can we validate that the required tools are available 177 // before we start? 178 if (!testTool(TOOL_DPKG_DEB, "1")){ 179 throw new ConfigException(MessageFormat.format( 180 I18N.getString("error.tool-not-found"), TOOL_DPKG_DEB), 181 I18N.getString("error.tool-not-found.advice")); 182 } 183 if (!testTool(TOOL_DPKG, "1")){ 184 throw new ConfigException(MessageFormat.format( 185 I18N.getString("error.tool-not-found"), TOOL_DPKG), 186 I18N.getString("error.tool-not-found.advice")); 187 } 188 189 190 // Show warning is license file is missing 191 String licenseFile = LICENSE_FILE.fetchFrom(params); 192 if (licenseFile == null) { 193 Log.verbose(I18N.getString("message.debs-like-licenses")); 194 } 195 } 196 197 @Override 198 protected File buildPackageBundle( 199 Map<String, String> replacementData, 200 Map<String, ? super Object> params, File outputParentDir) throws 201 PackagerException, IOException { 202 203 prepareProjectConfig(replacementData, params); 204 adjustPermissionsRecursive(createMetaPackage(params).sourceRoot().toFile()); 205 return buildDeb(params, outputParentDir); 206 } 207 208 /* 209 * set permissions with a string like "rwxr-xr-x" 210 * 211 * This cannot be directly backport to 22u which is built with 1.6 212 */ 213 private void setPermissions(File file, String permissions) { 214 Set<PosixFilePermission> filePermissions = 215 PosixFilePermissions.fromString(permissions); 216 try { 217 if (file.exists()) { 218 Files.setPosixFilePermissions(file.toPath(), filePermissions); 219 } 220 } catch (IOException ex) { 221 Log.error(ex.getMessage()); 222 Log.verbose(ex); 223 } 224 225 } 226 227 private static String getDebArch() throws IOException { 228 try (var baos = new ByteArrayOutputStream(); 229 var ps = new PrintStream(baos)) { 230 var pb = new ProcessBuilder(TOOL_DPKG, "--print-architecture"); 231 IOUtils.exec(pb, false, ps); 232 return baos.toString().split("\n", 2)[0]; 233 } 234 } 235 236 public static boolean isDebian() { 237 // we are just going to run "dpkg -s coreutils" and assume Debian 238 // or deritive if no error is returned. 239 var pb = new ProcessBuilder(TOOL_DPKG, "-s", "coreutils"); 240 try { 241 int ret = pb.start().waitFor(); 242 return (ret == 0); 243 } catch (IOException | InterruptedException e) { 244 // just fall thru 245 } 246 return false; 247 } 248 249 private void adjustPermissionsRecursive(File dir) throws IOException { 250 Files.walkFileTree(dir.toPath(), new SimpleFileVisitor<Path>() { 251 @Override 252 public FileVisitResult visitFile(Path file, 253 BasicFileAttributes attrs) 254 throws IOException { 255 if (file.endsWith(".so") || !Files.isExecutable(file)) { 256 setPermissions(file.toFile(), "rw-r--r--"); 257 } else if (Files.isExecutable(file)) { 258 setPermissions(file.toFile(), "rwxr-xr-x"); 259 } 260 return FileVisitResult.CONTINUE; 261 } 262 263 @Override 264 public FileVisitResult postVisitDirectory(Path dir, IOException e) 265 throws IOException { 266 if (e == null) { 267 setPermissions(dir.toFile(), "rwxr-xr-x"); 268 return FileVisitResult.CONTINUE; 269 } else { 270 // directory iteration failed 271 throw e; 272 } 273 } 274 }); 275 } 276 277 private class DebianFile { 278 279 DebianFile(Path dstFilePath, String comment) { 280 this.dstFilePath = dstFilePath; 281 this.comment = comment; 282 } 283 284 DebianFile setExecutable() { 285 permissions = "rwxr-xr-x"; 286 return this; 287 } 288 289 void create(Map<String, String> data, Map<String, ? super Object> params) 290 throws IOException { 291 Files.createDirectories(dstFilePath.getParent()); 292 try (Writer w = Files.newBufferedWriter(dstFilePath)) { 293 String content = preprocessTextResource( 294 dstFilePath.getFileName().toString(), 295 I18N.getString(comment), 296 "template." + dstFilePath.getFileName().toString(), 297 data, 298 VERBOSE.fetchFrom(params), 299 RESOURCE_DIR.fetchFrom(params)); 300 w.write(content); 301 } 302 if (permissions != null) { 303 setPermissions(dstFilePath.toFile(), permissions); 304 } 305 } 306 307 private final Path dstFilePath; 308 private final String comment; 309 private String permissions; 310 } 311 312 private void prepareProjectConfig(Map<String, String> data, 313 Map<String, ? super Object> params) throws IOException { 314 315 Path configDir = createMetaPackage(params).sourceRoot().resolve("DEBIAN"); 316 List<DebianFile> debianFiles = new ArrayList<>(); 317 debianFiles.add(new DebianFile( 318 configDir.resolve("control"), 319 "resource.deb-control-file")); 320 debianFiles.add(new DebianFile( 321 configDir.resolve("preinst"), 322 "resource.deb-preinstall-script").setExecutable()); 323 debianFiles.add(new DebianFile( 324 configDir.resolve("prerm"), 325 "resource.deb-prerm-script").setExecutable()); 326 debianFiles.add(new DebianFile( 327 configDir.resolve("postinst"), 328 "resource.deb-postinstall-script").setExecutable()); 329 debianFiles.add(new DebianFile( 330 configDir.resolve("postrm"), 331 "resource.deb-postrm-script").setExecutable()); 332 333 getConfig_CopyrightFile(params).getParentFile().mkdirs(); 334 String customCopyrightFile = COPYRIGHT_FILE.fetchFrom(params); 335 if (customCopyrightFile != null) { 336 IOUtils.copyFile(new File(customCopyrightFile), 337 getConfig_CopyrightFile(params)); 338 } else { 339 debianFiles.add(new DebianFile( 340 getConfig_CopyrightFile(params).toPath(), 341 "resource.copyright-file")); 342 } 343 344 for (DebianFile debianFile : debianFiles) { 345 debianFile.create(data, params); 346 } 347 } 348 349 @Override 350 protected Map<String, String> createReplacementData( 351 Map<String, ? super Object> params) throws IOException { 352 Map<String, String> data = new HashMap<>(); 353 354 data.put("APPLICATION_MAINTAINER", MAINTAINER.fetchFrom(params)); 355 data.put("APPLICATION_SECTION", SECTION.fetchFrom(params)); 356 data.put("APPLICATION_COPYRIGHT", COPYRIGHT.fetchFrom(params)); 357 data.put("APPLICATION_LICENSE_TEXT", LICENSE_TEXT.fetchFrom(params)); 358 data.put("APPLICATION_ARCH", getDebArch()); 359 data.put("APPLICATION_INSTALLED_SIZE", Long.toString( 360 createMetaPackage(params).sourceApplicationLayout().sizeInBytes() >> 10)); 361 362 return data; 363 } 364 365 private File getConfig_CopyrightFile(Map<String, ? super Object> params) { 366 PlatformPackage thePackage = createMetaPackage(params); 367 return thePackage.sourceRoot().resolve(Path.of(".", 368 LINUX_INSTALL_DIR.fetchFrom(params), PACKAGE_NAME.fetchFrom( 369 params), "share/doc/copyright")).toFile(); 370 } 371 372 private File buildDeb(Map<String, ? super Object> params, 373 File outdir) throws IOException { 374 File outFile = new File(outdir, 375 FULL_PACKAGE_NAME.fetchFrom(params)+".deb"); 376 Log.verbose(MessageFormat.format(I18N.getString( 377 "message.outputting-to-location"), outFile.getAbsolutePath())); 378 379 PlatformPackage thePackage = createMetaPackage(params); 380 381 // run dpkg 382 ProcessBuilder pb = new ProcessBuilder( 383 "fakeroot", TOOL_DPKG_DEB, "-b", 384 thePackage.sourceRoot().toString(), 385 outFile.getAbsolutePath()); 386 IOUtils.exec(pb); 387 388 Log.verbose(MessageFormat.format(I18N.getString( 389 "message.output-to-location"), outFile.getAbsolutePath())); 390 391 return outFile; 392 } 393 394 @Override 395 public String getName() { 396 return I18N.getString("deb.bundler.name"); 397 } 398 399 @Override 400 public String getID() { 401 return "deb"; 402 } 403 404 @Override 405 public boolean supported(boolean runtimeInstaller) { 406 if (Platform.getPlatform() == Platform.LINUX) { 407 if (testTool(TOOL_DPKG_DEB, "1")) { 408 return true; 409 } 410 } 411 return false; 412 } 413 414 @Override 415 public boolean isDefault() { 416 return isDebian(); 417 } 418 419 }