1 /* 2 * Copyright (c) 2012, 2015, 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 com.oracle.tools.packager.linux; 27 28 import com.oracle.tools.packager.AbstractImageBundler; 29 import com.oracle.tools.packager.BundlerParamInfo; 30 import com.oracle.tools.packager.JreUtils; 31 import com.oracle.tools.packager.JreUtils.Rule; 32 import com.oracle.tools.packager.StandardBundlerParam; 33 import com.oracle.tools.packager.Log; 34 import com.oracle.tools.packager.ConfigException; 35 import com.oracle.tools.packager.IOUtils; 36 import com.oracle.tools.packager.RelativeFileSet; 37 import com.oracle.tools.packager.UnsupportedPlatformException; 38 import com.sun.javafx.tools.packager.bundlers.BundleParams; 39 40 import java.io.File; 41 import java.io.FileNotFoundException; 42 import java.io.IOException; 43 import java.io.PrintStream; 44 import java.net.MalformedURLException; 45 import java.net.URL; 46 import java.text.MessageFormat; 47 import java.util.*; 48 49 import static com.oracle.tools.packager.StandardBundlerParam.*; 50 51 public class LinuxAppBundler extends AbstractImageBundler { 52 53 private static final ResourceBundle I18N = 54 ResourceBundle.getBundle(LinuxAppBundler.class.getName()); 55 56 protected static final String LINUX_BUNDLER_PREFIX = 57 BUNDLER_PREFIX + "linux" + File.separator; 58 private static final String EXECUTABLE_NAME = "JavaAppLauncher"; 59 private static final String LIBRARY_NAME = "libpackager.so"; 60 61 public static final BundlerParamInfo<File> ICON_PNG = new StandardBundlerParam<>( 62 I18N.getString("param.icon-png.name"), 63 I18N.getString("param.icon-png.description"), 64 "icon.png", 65 File.class, 66 params -> { 67 File f = ICON.fetchFrom(params); 68 if (f != null && !f.getName().toLowerCase().endsWith(".png")) { 69 Log.info(MessageFormat.format(I18N.getString("message.icon-not-png"), f)); 70 return null; 71 } 72 return f; 73 }, 74 (s, p) -> new File(s)); 75 76 public static final BundlerParamInfo<URL> RAW_EXECUTABLE_URL = new StandardBundlerParam<>( 77 I18N.getString("param.raw-executable-url.name"), 78 I18N.getString("param.raw-executable-url.description"), 79 "linux.launcher.url", 80 URL.class, 81 params -> LinuxResources.class.getResource(EXECUTABLE_NAME), 82 (s, p) -> { 83 try { 84 return new URL(s); 85 } catch (MalformedURLException e) { 86 Log.info(e.toString()); 87 return null; 88 } 89 }); 90 91 //Subsetting of JRE is restricted. 92 //JRE README defines what is allowed to strip: 93 // http://www.oracle.com/technetwork/java/javase/jre-8-readme-2095710.html 94 // 95 public static final BundlerParamInfo<Rule[]> LINUX_JRE_RULES = new StandardBundlerParam<>( 96 "", 97 "", 98 ".linux.runtime.rules", 99 Rule[].class, 100 params -> new Rule[]{ 101 Rule.prefixNeg("/bin"), 102 Rule.prefixNeg("/plugin"), 103 //Rule.prefixNeg("/lib/ext"), //need some of jars there for https to work 104 Rule.suffix("deploy.jar"), //take deploy.jar 105 Rule.prefixNeg("/lib/deploy"), 106 Rule.prefixNeg("/lib/desktop"), 107 Rule.substrNeg("libnpjp2.so") 108 }, 109 (s, p) -> null 110 ); 111 112 public static final BundlerParamInfo<RelativeFileSet> LINUX_RUNTIME = new StandardBundlerParam<>( 113 I18N.getString("param.runtime.name"), 114 I18N.getString("param.runtime.description"), 115 BundleParams.PARAM_RUNTIME, 116 RelativeFileSet.class, 117 params -> JreUtils.extractJreAsRelativeFileSet(System.getProperty("java.home"), 118 LINUX_JRE_RULES.fetchFrom(params)), 119 (s, p) -> JreUtils.extractJreAsRelativeFileSet(s, LINUX_JRE_RULES.fetchFrom(p)) 120 ); 121 122 @Override 123 public boolean validate(Map<String, ? super Object> p) throws UnsupportedPlatformException, ConfigException { 124 try { 125 if (p == null) throw new ConfigException( 126 I18N.getString("error.parameters-null"), 127 I18N.getString("error.parameters-null.advice")); 128 129 return doValidate(p); 130 } catch (RuntimeException re) { 131 if (re.getCause() instanceof ConfigException) { 132 throw (ConfigException) re.getCause(); 133 } else { 134 throw new ConfigException(re); 135 } 136 } 137 } 138 139 //used by chained bundlers to reuse validation logic 140 boolean doValidate(Map<String, ? super Object> p) throws UnsupportedPlatformException, ConfigException { 141 if (!System.getProperty("os.name").toLowerCase().startsWith("linux")) { 142 throw new UnsupportedPlatformException(); 143 } 144 145 StandardBundlerParam.validateMainClassInfoFromAppResources(p); 146 147 Map<String, String> userJvmOptions = USER_JVM_OPTIONS.fetchFrom(p); 148 if (userJvmOptions != null) { 149 for (Map.Entry<String, String> entry : userJvmOptions.entrySet()) { 150 if (entry.getValue() == null || entry.getValue().isEmpty()) { 151 throw new ConfigException( 152 MessageFormat.format(I18N.getString("error.empty-user-jvm-option-value"), entry.getKey()), 153 I18N.getString("error.empty-user-jvm-option-value.advice")); 154 } 155 } 156 } 157 158 if (RAW_EXECUTABLE_URL.fetchFrom(p) == null) { 159 throw new ConfigException( 160 I18N.getString("error.no-linux-resources"), 161 I18N.getString("error.no-linux-resources.advice")); 162 } 163 164 if (MAIN_JAR.fetchFrom(p) == null) { 165 throw new ConfigException( 166 I18N.getString("error.no-application-jar"), 167 I18N.getString("error.no-application-jar.advice")); 168 } 169 170 //validate required inputs 171 testRuntime(LINUX_RUNTIME.fetchFrom(p), new String[] { 172 "lib/[^/]+/[^/]+/libjvm.so", // most reliable 173 "lib/rt.jar", // fallback canary for JDK 8 174 }); 175 if (USE_FX_PACKAGING.fetchFrom(p)) { 176 testRuntime(LINUX_RUNTIME.fetchFrom(p), new String[] {"lib/ext/jfxrt.jar", "lib/jfxrt.jar"}); 177 } 178 179 return true; 180 } 181 182 //it is static for the sake of sharing with "installer" bundlers 183 // that may skip calls to validate/bundle in this class! 184 public static File getRootDir(File outDir, Map<String, ? super Object> p) { 185 return new File(outDir, APP_FS_NAME.fetchFrom(p)); 186 } 187 188 public static String getLauncherName(Map<String, ? super Object> p) { 189 return APP_FS_NAME.fetchFrom(p); 190 } 191 192 public static String getLauncherCfgName(Map<String, ? super Object> p) { 193 return "app/" + APP_FS_NAME.fetchFrom(p) +".cfg"; 194 } 195 196 File doBundle(Map<String, ? super Object> p, File outputDirectory, boolean dependentTask) { 197 Map<String, ? super Object> originalParams = new HashMap<>(p); 198 try { 199 if (!outputDirectory.isDirectory() && !outputDirectory.mkdirs()) { 200 throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-create-output-dir"), outputDirectory.getAbsolutePath())); 201 } 202 if (!outputDirectory.canWrite()) { 203 throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-write-to-output-dir"), outputDirectory.getAbsolutePath())); 204 } 205 206 // Create directory structure 207 File rootDirectory = getRootDir(outputDirectory, p); 208 IOUtils.deleteRecursive(rootDirectory); 209 rootDirectory.mkdirs(); 210 211 if (!dependentTask) { 212 Log.info(MessageFormat.format(I18N.getString("message.creating-bundle-location"), rootDirectory.getAbsolutePath())); 213 } 214 215 File runtimeDirectory = new File(rootDirectory, "runtime"); 216 217 File appDirectory = new File(rootDirectory, "app"); 218 appDirectory.mkdirs(); 219 220 // create the primary launcher 221 createLauncherForEntryPoint(p, rootDirectory); 222 223 // Copy library to the launcher folder 224 IOUtils.copyFromURL( 225 LinuxResources.class.getResource(LIBRARY_NAME), 226 new File(rootDirectory, LIBRARY_NAME)); 227 228 // create the secondary launchers, if any 229 List<Map<String, ? super Object>> entryPoints = StandardBundlerParam.SECONDARY_LAUNCHERS.fetchFrom(p); 230 for (Map<String, ? super Object> entryPoint : entryPoints) { 231 Map<String, ? super Object> tmp = new HashMap<>(originalParams); 232 tmp.putAll(entryPoint); 233 createLauncherForEntryPoint(tmp, rootDirectory); 234 } 235 236 // Copy runtime to PlugIns folder 237 copyRuntime(p, runtimeDirectory); 238 239 // Copy class path entries to Java folder 240 copyApplication(p, appDirectory); 241 242 // Copy icon to Resources folder 243 //FIXME copyIcon(resourcesDirectory); 244 245 return rootDirectory; 246 } catch (IOException ex) { 247 Log.info("Exception: "+ex); 248 Log.debug(ex); 249 return null; 250 } 251 } 252 253 private void createLauncherForEntryPoint(Map<String, ? super Object> p, File rootDir) throws IOException { 254 // Copy executable to Linux folder 255 File executableFile = new File(rootDir, getLauncherName(p)); 256 IOUtils.copyFromURL( 257 RAW_EXECUTABLE_URL.fetchFrom(p), 258 executableFile); 259 260 executableFile.setExecutable(true, false); 261 executableFile.setWritable(true, true); //for str 262 263 // Generate launcher .cfg file 264 if (LAUNCHER_CFG_FORMAT.fetchFrom(p).equals(CFG_FORMAT_PROPERTIES)) { 265 writeCfgFile(p, rootDir); 266 } else { 267 writeCfgFile(p, new File(rootDir, getLauncherCfgName(p)), "$APPDIR/runtime"); 268 } 269 } 270 271 private void copyApplication(Map<String, ? super Object> params, File appDirectory) throws IOException { 272 List<RelativeFileSet> appResourcesList = APP_RESOURCES_LIST.fetchFrom(params); 273 if (appResourcesList == null) { 274 throw new RuntimeException("Null app resources?"); 275 } 276 for (RelativeFileSet appResources : appResourcesList) { 277 if (appResources == null) { 278 throw new RuntimeException("Null app resources?"); 279 } 280 File srcdir = appResources.getBaseDirectory(); 281 for (String fname : appResources.getIncludedFiles()) { 282 IOUtils.copyFile( 283 new File(srcdir, fname), new File(appDirectory, fname)); 284 } 285 } 286 } 287 288 private void writeCfgFile(Map<String, ? super Object> params, File rootDir) throws FileNotFoundException { 289 File cfgFile = new File(rootDir, getLauncherCfgName(params)); 290 291 cfgFile.delete(); 292 PrintStream out = new PrintStream(cfgFile); 293 if (LINUX_RUNTIME.fetchFrom(params) == null) { 294 out.println("app.runtime="); 295 } else { 296 out.println("app.runtime=$APPDIR/runtime"); 297 } 298 out.println("app.mainjar=" + MAIN_JAR.fetchFrom(params).getIncludedFiles().iterator().next()); 299 out.println("app.version=" + VERSION.fetchFrom(params)); 300 301 //use '/' in the class name (instead of '.' to simplify native code 302 out.println("app.mainclass=" + 303 MAIN_CLASS.fetchFrom(params).replaceAll("\\.", "/")); 304 305 StringBuilder macroedPath = new StringBuilder(); 306 for (String s : CLASSPATH.fetchFrom(params).split("[ ;:]+")) { 307 macroedPath.append(s); 308 macroedPath.append(":"); 309 } 310 macroedPath.deleteCharAt(macroedPath.length() - 1); 311 out.println("app.classpath=" + macroedPath.toString()); 312 313 List<String> jvmargs = JVM_OPTIONS.fetchFrom(params); 314 int idx = 1; 315 for (String a : jvmargs) { 316 out.println("jvmarg."+idx+"="+a); 317 idx++; 318 } 319 Map<String, String> jvmProps = JVM_PROPERTIES.fetchFrom(params); 320 for (Map.Entry<String, String> entry : jvmProps.entrySet()) { 321 out.println("jvmarg."+idx+"=-D"+entry.getKey()+"="+entry.getValue()); 322 idx++; 323 } 324 325 String preloader = PRELOADER_CLASS.fetchFrom(params); 326 if (preloader != null) { 327 out.println("jvmarg."+idx+"=-Djavafx.preloader="+preloader); 328 } 329 330 //app.id required for setting user preferences (Java Preferences API) 331 out.println("app.preferences.id=" + PREFERENCES_ID.fetchFrom(params)); 332 out.println("app.identifier=" + IDENTIFIER.fetchFrom(params)); 333 334 Map<String, String> overridableJVMOptions = USER_JVM_OPTIONS.fetchFrom(params); 335 idx = 1; 336 for (Map.Entry<String, String> arg: overridableJVMOptions.entrySet()) { 337 if (arg.getKey() == null || arg.getValue() == null) { 338 Log.info(I18N.getString("message.jvm-user-arg-is-null")); 339 } 340 else { 341 out.println("jvmuserarg."+idx+".name="+arg.getKey()); 342 out.println("jvmuserarg."+idx+".value="+arg.getValue()); 343 } 344 idx++; 345 } 346 347 // add command line args 348 List<String> args = ARGUMENTS.fetchFrom(params); 349 idx = 1; 350 for (String a : args) { 351 out.println("arg."+idx+"="+a); 352 idx++; 353 } 354 355 out.close(); 356 } 357 358 private void copyRuntime(Map<String, ? super Object> params, File runtimeDirectory) throws IOException { 359 RelativeFileSet runtime = LINUX_RUNTIME.fetchFrom(params); 360 if (runtime == null) { 361 //request to use system runtime 362 return; 363 } 364 runtimeDirectory.mkdirs(); 365 366 File srcdir = runtime.getBaseDirectory(); 367 Set<String> filesToCopy = runtime.getIncludedFiles(); 368 for (String fname : filesToCopy) { 369 IOUtils.copyFile( 370 new File(srcdir, fname), new File(runtimeDirectory, fname)); 371 } 372 } 373 374 @Override 375 public String getName() { 376 return I18N.getString("bundler.name"); 377 } 378 379 @Override 380 public String getDescription() { 381 return I18N.getString("bundler.description"); 382 } 383 384 @Override 385 public String getID() { 386 return "linux.app"; 387 } 388 389 @Override 390 public String getBundleType() { 391 return "IMAGE"; 392 } 393 394 @Override 395 public Collection<BundlerParamInfo<?>> getBundleParameters() { 396 return getAppBundleParameters(); 397 } 398 399 public static Collection<BundlerParamInfo<?>> getAppBundleParameters() { 400 return Arrays.asList( 401 APP_NAME, 402 APP_RESOURCES, 403 // APP_RESOURCES_LIST, // ?? 404 ARGUMENTS, 405 CLASSPATH, 406 JVM_OPTIONS, 407 JVM_PROPERTIES, 408 LINUX_RUNTIME, 409 MAIN_CLASS, 410 MAIN_JAR, 411 PREFERENCES_ID, 412 PRELOADER_CLASS, 413 USER_JVM_OPTIONS, 414 VERSION 415 ); 416 } 417 418 @Override 419 public File execute(Map<String, ? super Object> params, File outputParentDir) { 420 return doBundle(params, outputParentDir, false); 421 } 422 423 @Override 424 protected String getCacheLocation(Map<String, ? super Object> params) { 425 return "$APPDIR/"; 426 } 427 }