1 /* 2 * Copyright (c) 2019, 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 package jdk.jpackage.internal; 26 27 import java.io.IOException; 28 import java.nio.file.InvalidPathException; 29 import java.nio.file.Path; 30 import java.nio.file.Files; 31 import java.text.MessageFormat; 32 import java.util.Collections; 33 import java.util.HashMap; 34 import java.util.List; 35 import java.util.Map; 36 import java.util.Objects; 37 import java.util.Set; 38 import java.util.function.Function; 39 import java.util.function.Predicate; 40 import java.util.function.Supplier; 41 import java.util.stream.Collectors; 42 import java.util.stream.Stream; 43 import static jdk.jpackage.internal.DesktopIntegration.*; 44 import static jdk.jpackage.internal.StandardBundlerParam.PREDEFINED_RUNTIME_IMAGE; 45 import static jdk.jpackage.internal.StandardBundlerParam.VERSION; 46 import static jdk.jpackage.internal.StandardBundlerParam.RELEASE; 47 import static jdk.jpackage.internal.StandardBundlerParam.VENDOR; 48 import static jdk.jpackage.internal.StandardBundlerParam.DESCRIPTION; 49 import static jdk.jpackage.internal.StandardBundlerParam.INSTALL_DIR; 50 51 abstract class LinuxPackageBundler extends AbstractBundler { 52 LinuxPackageBundler(BundlerParamInfo<String> packageName)53 LinuxPackageBundler(BundlerParamInfo<String> packageName) { 54 this.packageName = packageName; 55 appImageBundler = new LinuxAppBundler().setDependentTask(true); 56 } 57 58 @Override validate(Map<String, ? super Object> params)59 final public boolean validate(Map<String, ? super Object> params) 60 throws ConfigException { 61 62 // run basic validation to ensure requirements are met 63 // we are not interested in return code, only possible exception 64 appImageBundler.validate(params); 65 66 validateInstallDir(LINUX_INSTALL_DIR.fetchFrom(params)); 67 68 FileAssociation.verify(FileAssociation.fetchFrom(params)); 69 70 // If package name has some restrictions, the string converter will 71 // throw an exception if invalid 72 packageName.getStringConverter().apply(packageName.fetchFrom(params), 73 params); 74 75 for (var validator: getToolValidators(params)) { 76 ConfigException ex = validator.validate(); 77 if (ex != null) { 78 throw ex; 79 } 80 } 81 82 if (!isDefault()) { 83 withFindNeededPackages = false; 84 Log.verbose(MessageFormat.format(I18N.getString( 85 "message.not-default-bundler-no-dependencies-lookup"), 86 getName())); 87 } else { 88 withFindNeededPackages = LibProvidersLookup.supported(); 89 if (!withFindNeededPackages) { 90 final String advice; 91 if ("deb".equals(getID())) { 92 advice = "message.deb-ldd-not-available.advice"; 93 } else { 94 advice = "message.rpm-ldd-not-available.advice"; 95 } 96 // Let user know package dependencies will not be generated. 97 Log.error(String.format("%s\n%s", I18N.getString( 98 "message.ldd-not-available"), I18N.getString(advice))); 99 } 100 } 101 102 // Packaging specific validation 103 doValidate(params); 104 105 return true; 106 } 107 108 @Override getBundleType()109 final public String getBundleType() { 110 return "INSTALLER"; 111 } 112 113 @Override execute(Map<String, ? super Object> params, Path outputParentDir)114 final public Path execute(Map<String, ? super Object> params, 115 Path outputParentDir) throws PackagerException { 116 IOUtils.writableOutputDir(outputParentDir); 117 118 PlatformPackage thePackage = createMetaPackage(params); 119 120 Function<Path, ApplicationLayout> initAppImageLayout = imageRoot -> { 121 ApplicationLayout layout = appImageLayout(params); 122 layout.pathGroup().setPath(new Object(), 123 AppImageFile.getPathInAppImage(Path.of(""))); 124 return layout.resolveAt(imageRoot); 125 }; 126 127 try { 128 Path appImage = StandardBundlerParam.getPredefinedAppImage(params); 129 130 // we either have an application image or need to build one 131 if (appImage != null) { 132 initAppImageLayout.apply(appImage).copy( 133 thePackage.sourceApplicationLayout()); 134 } else { 135 final Path srcAppImageRoot = thePackage.sourceRoot().resolve("src"); 136 appImage = appImageBundler.execute(params, srcAppImageRoot); 137 ApplicationLayout srcAppLayout = initAppImageLayout.apply( 138 appImage); 139 if (appImage.equals(PREDEFINED_RUNTIME_IMAGE.fetchFrom(params))) { 140 // Application image points to run-time image. 141 // Copy it. 142 srcAppLayout.copy(thePackage.sourceApplicationLayout()); 143 } else { 144 // Application image is a newly created directory tree. 145 // Move it. 146 srcAppLayout.move(thePackage.sourceApplicationLayout()); 147 IOUtils.deleteRecursive(srcAppImageRoot); 148 } 149 } 150 151 desktopIntegration = DesktopIntegration.create(thePackage, params); 152 153 Map<String, String> data = createDefaultReplacementData(params); 154 if (desktopIntegration != null) { 155 data.putAll(desktopIntegration.create()); 156 } else { 157 Stream.of(DESKTOP_COMMANDS_INSTALL, DESKTOP_COMMANDS_UNINSTALL, 158 UTILITY_SCRIPTS).forEach(v -> data.put(v, "")); 159 } 160 161 data.putAll(createReplacementData(params)); 162 163 Path packageBundle = buildPackageBundle(Collections.unmodifiableMap( 164 data), params, outputParentDir); 165 166 verifyOutputBundle(params, packageBundle).stream() 167 .filter(Objects::nonNull) 168 .forEachOrdered(ex -> { 169 Log.verbose(ex.getLocalizedMessage()); 170 Log.verbose(ex.getAdvice()); 171 }); 172 173 return packageBundle; 174 } catch (IOException ex) { 175 Log.verbose(ex); 176 throw new PackagerException(ex); 177 } 178 } 179 getListOfNeededPackages( Map<String, ? super Object> params)180 private List<String> getListOfNeededPackages( 181 Map<String, ? super Object> params) throws IOException { 182 183 PlatformPackage thePackage = createMetaPackage(params); 184 185 final List<String> xdgUtilsPackage; 186 if (desktopIntegration != null) { 187 xdgUtilsPackage = desktopIntegration.requiredPackages(); 188 } else { 189 xdgUtilsPackage = Collections.emptyList(); 190 } 191 192 final List<String> neededLibPackages; 193 if (withFindNeededPackages && Files.exists(thePackage.sourceRoot())) { 194 LibProvidersLookup lookup = new LibProvidersLookup(); 195 initLibProvidersLookup(params, lookup); 196 197 neededLibPackages = lookup.execute(thePackage.sourceRoot()); 198 } else { 199 neededLibPackages = Collections.emptyList(); 200 if (!Files.exists(thePackage.sourceRoot())) { 201 Log.info(I18N.getString("warning.foreign-app-image")); 202 } 203 } 204 205 // Merge all package lists together. 206 // Filter out empty names, sort and remove duplicates. 207 List<String> result = Stream.of(xdgUtilsPackage, neededLibPackages).flatMap( 208 List::stream).filter(Predicate.not(String::isEmpty)).sorted().distinct().collect( 209 Collectors.toList()); 210 211 Log.verbose(String.format("Required packages: %s", result)); 212 213 return result; 214 } 215 createDefaultReplacementData( Map<String, ? super Object> params)216 private Map<String, String> createDefaultReplacementData( 217 Map<String, ? super Object> params) throws IOException { 218 Map<String, String> data = new HashMap<>(); 219 220 data.put("APPLICATION_PACKAGE", createMetaPackage(params).name()); 221 data.put("APPLICATION_VENDOR", VENDOR.fetchFrom(params)); 222 data.put("APPLICATION_VERSION", VERSION.fetchFrom(params)); 223 data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params)); 224 data.put("APPLICATION_RELEASE", RELEASE.fetchFrom(params)); 225 226 String defaultDeps = String.join(", ", getListOfNeededPackages(params)); 227 String customDeps = LINUX_PACKAGE_DEPENDENCIES.fetchFrom(params).strip(); 228 if (!customDeps.isEmpty() && !defaultDeps.isEmpty()) { 229 customDeps = ", " + customDeps; 230 } 231 data.put("PACKAGE_DEFAULT_DEPENDENCIES", defaultDeps); 232 data.put("PACKAGE_CUSTOM_DEPENDENCIES", customDeps); 233 234 return data; 235 } 236 verifyOutputBundle( Map<String, ? super Object> params, Path packageBundle)237 abstract protected List<ConfigException> verifyOutputBundle( 238 Map<String, ? super Object> params, Path packageBundle); 239 initLibProvidersLookup( Map<String, ? super Object> params, LibProvidersLookup libProvidersLookup)240 abstract protected void initLibProvidersLookup( 241 Map<String, ? super Object> params, 242 LibProvidersLookup libProvidersLookup); 243 getToolValidators( Map<String, ? super Object> params)244 abstract protected List<ToolValidator> getToolValidators( 245 Map<String, ? super Object> params); 246 doValidate(Map<String, ? super Object> params)247 abstract protected void doValidate(Map<String, ? super Object> params) 248 throws ConfigException; 249 createReplacementData( Map<String, ? super Object> params)250 abstract protected Map<String, String> createReplacementData( 251 Map<String, ? super Object> params) throws IOException; 252 buildPackageBundle( Map<String, String> replacementData, Map<String, ? super Object> params, Path outputParentDir)253 abstract protected Path buildPackageBundle( 254 Map<String, String> replacementData, 255 Map<String, ? super Object> params, Path outputParentDir) throws 256 PackagerException, IOException; 257 createMetaPackage( Map<String, ? super Object> params)258 final protected PlatformPackage createMetaPackage( 259 Map<String, ? super Object> params) { 260 261 Supplier<ApplicationLayout> packageLayout = () -> { 262 String installDir = LINUX_INSTALL_DIR.fetchFrom(params); 263 if (isInstallDirInUsrTree(installDir)) { 264 return ApplicationLayout.linuxUsrTreePackageImage( 265 Path.of("/").relativize(Path.of(installDir)), 266 packageName.fetchFrom(params)); 267 } 268 return appImageLayout(params); 269 }; 270 271 return new PlatformPackage() { 272 @Override 273 public String name() { 274 return packageName.fetchFrom(params); 275 } 276 277 @Override 278 public Path sourceRoot() { 279 return IMAGES_ROOT.fetchFrom(params).toAbsolutePath(); 280 } 281 282 @Override 283 public ApplicationLayout sourceApplicationLayout() { 284 return packageLayout.get().resolveAt( 285 applicationInstallDir(sourceRoot())); 286 } 287 288 @Override 289 public ApplicationLayout installedApplicationLayout() { 290 return packageLayout.get().resolveAt( 291 applicationInstallDir(Path.of("/"))); 292 } 293 294 private Path applicationInstallDir(Path root) { 295 String installRoot = LINUX_INSTALL_DIR.fetchFrom(params); 296 if (isInstallDirInUsrTree(installRoot)) { 297 return root; 298 } 299 300 Path installDir = Path.of(installRoot, name()); 301 if (installDir.isAbsolute()) { 302 installDir = Path.of("." + installDir.toString()).normalize(); 303 } 304 return root.resolve(installDir); 305 } 306 }; 307 } 308 309 private ApplicationLayout appImageLayout( 310 Map<String, ? super Object> params) { 311 if (StandardBundlerParam.isRuntimeInstaller(params)) { 312 return ApplicationLayout.javaRuntime(); 313 } 314 return ApplicationLayout.linuxAppImage(); 315 } 316 317 private static void validateInstallDir(String installDir) throws 318 ConfigException { 319 320 if (installDir.isEmpty()) { 321 throw new ConfigException(MessageFormat.format(I18N.getString( 322 "error.invalid-install-dir"), "/"), null); 323 } 324 325 boolean valid = false; 326 try { 327 final Path installDirPath = Path.of(installDir); 328 valid = installDirPath.isAbsolute(); 329 if (valid && !installDirPath.normalize().toString().equals( 330 installDirPath.toString())) { 331 // Don't allow '/opt/foo/..' or /opt/. 332 valid = false; 333 } 334 } catch (InvalidPathException ex) { 335 } 336 337 if (!valid) { 338 throw new ConfigException(MessageFormat.format(I18N.getString( 339 "error.invalid-install-dir"), installDir), null); 340 } 341 } 342 343 protected static boolean isInstallDirInUsrTree(String installDir) { 344 return Set.of("/usr/local", "/usr").contains(installDir); 345 } 346 347 private final BundlerParamInfo<String> packageName; 348 private final Bundler appImageBundler; 349 private boolean withFindNeededPackages; 350 private DesktopIntegration desktopIntegration; 351 352 private static final BundlerParamInfo<String> LINUX_PACKAGE_DEPENDENCIES = 353 new StandardBundlerParam<>( 354 Arguments.CLIOptions.LINUX_PACKAGE_DEPENDENCIES.getId(), 355 String.class, 356 params -> "", 357 (s, p) -> s 358 ); 359 360 static final BundlerParamInfo<String> LINUX_INSTALL_DIR = 361 new StandardBundlerParam<>( 362 "linux-install-dir", 363 String.class, 364 params -> { 365 String dir = INSTALL_DIR.fetchFrom(params); 366 if (dir != null) { 367 if (dir.endsWith("/")) { 368 dir = dir.substring(0, dir.length()-1); 369 } 370 return dir; 371 } 372 return "/opt"; 373 }, 374 (s, p) -> s 375 ); 376 } 377