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