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