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 desktopIntegration = DesktopIntegration.create(thePackage, params); 139 140 Map<String, String> data = createDefaultReplacementData(params); 141 if (desktopIntegration != null) { 142 data.putAll(desktopIntegration.create()); 143 } else { 144 Stream.of(DESKTOP_COMMANDS_INSTALL, DESKTOP_COMMANDS_UNINSTALL, 145 UTILITY_SCRIPTS).forEach(v -> data.put(v, "")); 146 } 147 148 data.putAll(createReplacementData(params)); 149 150 File packageBundle = buildPackageBundle(Collections.unmodifiableMap( 151 data), params, outputParentDir); 152 153 verifyOutputBundle(params, packageBundle.toPath()).stream() 154 .filter(Objects::nonNull) 155 .forEachOrdered(ex -> { 156 Log.verbose(ex.getLocalizedMessage()); 157 Log.verbose(ex.getAdvice()); 158 }); 159 160 return packageBundle; 161 } catch (IOException ex) { 162 Log.verbose(ex); 163 throw new PackagerException(ex); 164 } 165 } 166 167 private List<String> getListOfNeededPackages( 168 Map<String, ? super Object> params) throws IOException { 169 170 PlatformPackage thePackage = createMetaPackage(params); 171 172 final List<String> xdgUtilsPackage; 173 if (desktopIntegration != null) { 174 xdgUtilsPackage = desktopIntegration.requiredPackages(); 175 } else { 176 xdgUtilsPackage = Collections.emptyList(); 177 } 178 179 final List<String> neededLibPackages; 180 if (withFindNeededPackages) { 181 LibProvidersLookup lookup = new LibProvidersLookup(); 182 initLibProvidersLookup(params, lookup); 183 184 neededLibPackages = lookup.execute(thePackage.sourceRoot()); 185 } else { 186 neededLibPackages = Collections.emptyList(); 187 } 188 189 // Merge all package lists together. 190 // Filter out empty names, sort and remove duplicates. 191 List<String> result = Stream.of(xdgUtilsPackage, neededLibPackages).flatMap( 192 List::stream).filter(Predicate.not(String::isEmpty)).sorted().distinct().collect( 193 Collectors.toList()); 194 195 Log.verbose(String.format("Required packages: %s", result)); 196 197 return result; 198 } 199 200 private Map<String, String> createDefaultReplacementData( 201 Map<String, ? super Object> params) throws IOException { 202 Map<String, String> data = new HashMap<>(); 203 204 data.put("APPLICATION_PACKAGE", createMetaPackage(params).name()); 205 data.put("APPLICATION_VENDOR", VENDOR.fetchFrom(params)); 206 data.put("APPLICATION_VERSION", VERSION.fetchFrom(params)); 207 data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params)); 208 data.put("APPLICATION_RELEASE", RELEASE.fetchFrom(params)); 209 210 String defaultDeps = String.join(", ", getListOfNeededPackages(params)); 211 String customDeps = LINUX_PACKAGE_DEPENDENCIES.fetchFrom(params).strip(); 212 if (!customDeps.isEmpty() && !defaultDeps.isEmpty()) { 213 customDeps = ", " + customDeps; 214 } 215 data.put("PACKAGE_DEFAULT_DEPENDENCIES", defaultDeps); 216 data.put("PACKAGE_CUSTOM_DEPENDENCIES", customDeps); 217 218 return data; 219 } 220 221 abstract protected List<ConfigException> verifyOutputBundle( 222 Map<String, ? super Object> params, Path packageBundle); 223 224 abstract protected void initLibProvidersLookup( 225 Map<String, ? super Object> params, 226 LibProvidersLookup libProvidersLookup); 227 228 abstract protected List<ToolValidator> getToolValidators( 229 Map<String, ? super Object> params); 230 231 abstract protected void doValidate(Map<String, ? super Object> params) 232 throws ConfigException; 233 234 abstract protected Map<String, String> createReplacementData( 235 Map<String, ? super Object> params) throws IOException; 236 237 abstract protected File buildPackageBundle( 238 Map<String, String> replacementData, 239 Map<String, ? super Object> params, File outputParentDir) throws 240 PackagerException, IOException; 241 242 final protected PlatformPackage createMetaPackage( 243 Map<String, ? super Object> params) { 244 return new PlatformPackage() { 245 @Override 246 public String name() { 247 return packageName.fetchFrom(params); 248 } 249 250 @Override 251 public Path sourceRoot() { 252 return IMAGES_ROOT.fetchFrom(params).toPath().toAbsolutePath(); 253 } 254 255 @Override 256 public ApplicationLayout sourceApplicationLayout() { 257 return appImageLayout(params).resolveAt( 258 applicationInstallDir(sourceRoot())); 259 } 260 261 @Override 262 public ApplicationLayout installedApplicationLayout() { 263 return appImageLayout(params).resolveAt( 264 applicationInstallDir(Path.of("/"))); 265 } 266 267 private Path applicationInstallDir(Path root) { 268 Path installDir = Path.of(LINUX_INSTALL_DIR.fetchFrom(params), 269 name()); 270 if (installDir.isAbsolute()) { 271 installDir = Path.of("." + installDir.toString()).normalize(); 272 } 273 return root.resolve(installDir); 274 } 275 }; 276 } 277 278 private ApplicationLayout appImageLayout( 279 Map<String, ? super Object> params) { 280 if (StandardBundlerParam.isRuntimeInstaller(params)) { 281 return ApplicationLayout.javaRuntime(); 282 } 283 return ApplicationLayout.linuxAppImage(); 284 } 285 286 private static void validateInstallDir(String installDir) throws 287 ConfigException { 288 if (installDir.startsWith("/usr/") || installDir.equals("/usr")) { 289 throw new ConfigException(MessageFormat.format(I18N.getString( 290 "error.unsupported-install-dir"), installDir), null); 291 } 292 293 if (installDir.isEmpty()) { 294 throw new ConfigException(MessageFormat.format(I18N.getString( 295 "error.invalid-install-dir"), "/"), null); 296 } 297 298 boolean valid = false; 299 try { 300 final Path installDirPath = Path.of(installDir); 301 valid = installDirPath.isAbsolute(); 302 if (valid && !installDirPath.normalize().toString().equals( 303 installDirPath.toString())) { 304 // Don't allow '/opt/foo/..' or /opt/. 305 valid = false; 306 } 307 } catch (InvalidPathException ex) { 308 } 309 310 if (!valid) { 311 throw new ConfigException(MessageFormat.format(I18N.getString( 312 "error.invalid-install-dir"), installDir), null); 313 } 314 } 315 316 private static void validateFileAssociations( 317 List<Map<String, ? super Object>> associations) throws 318 ConfigException { 319 // only one mime type per association, at least one file extention 320 int assocIdx = 0; 321 for (var assoc : associations) { 322 ++assocIdx; 323 List<String> mimes = FA_CONTENT_TYPE.fetchFrom(assoc); 324 if (mimes == null || mimes.isEmpty()) { 325 String msgKey = "error.no-content-types-for-file-association"; 326 throw new ConfigException( 327 MessageFormat.format(I18N.getString(msgKey), assocIdx), 328 I18N.getString(msgKey + ".advise")); 329 330 } 331 332 if (mimes.size() > 1) { 333 String msgKey = "error.too-many-content-types-for-file-association"; 334 throw new ConfigException( 335 MessageFormat.format(I18N.getString(msgKey), assocIdx), 336 I18N.getString(msgKey + ".advise")); 337 } 338 } 339 } 340 341 private final BundlerParamInfo<String> packageName; 342 private boolean withFindNeededPackages; 343 private DesktopIntegration desktopIntegration; 344 345 private static final BundlerParamInfo<LinuxAppBundler> APP_BUNDLER = 346 new StandardBundlerParam<>( 347 "linux.app.bundler", 348 LinuxAppBundler.class, 349 (params) -> new LinuxAppBundler(), 350 null 351 ); 352 353 }