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.AbstractBundler; 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 AbstractBundler { 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 PkgInfo 264 writePkgInfo(p, rootDir); 265 } 266 267 private void copyApplication(Map<String, ? super Object> params, File appDirectory) throws IOException { 268 List<RelativeFileSet> appResourcesList = APP_RESOURCES_LIST.fetchFrom(params); 269 if (appResourcesList == null) { 270 throw new RuntimeException("Null app resources?"); 271 } 272 for (RelativeFileSet appResources : appResourcesList) { 273 if (appResources == null) { 274 throw new RuntimeException("Null app resources?"); 275 } 276 File srcdir = appResources.getBaseDirectory(); 277 for (String fname : appResources.getIncludedFiles()) { 278 IOUtils.copyFile( 279 new File(srcdir, fname), new File(appDirectory, fname)); 280 } 281 } 282 } 283 284 private void writePkgInfo(Map<String, ? super Object> params, File rootDir) throws FileNotFoundException { 285 File pkgInfoFile = new File(rootDir, getLauncherCfgName(params)); 286 287 pkgInfoFile.delete(); 288 PrintStream out = new PrintStream(pkgInfoFile); 289 if (LINUX_RUNTIME.fetchFrom(params) == null) { 290 out.println("app.runtime="); 291 } else { 292 out.println("app.runtime=$APPDIR/runtime"); 293 } 294 out.println("app.mainjar=" + MAIN_JAR.fetchFrom(params).getIncludedFiles().iterator().next()); 295 out.println("app.version=" + VERSION.fetchFrom(params)); 296 297 //use '/' in the class name (instead of '.' to simplify native code 298 out.println("app.mainclass=" + 299 MAIN_CLASS.fetchFrom(params).replaceAll("\\.", "/")); 300 301 StringBuilder macroedPath = new StringBuilder(); 302 for (String s : CLASSPATH.fetchFrom(params).split("[ ;:]+")) { 303 macroedPath.append(s); 304 macroedPath.append(":"); 305 } 306 macroedPath.deleteCharAt(macroedPath.length() - 1); 307 out.println("app.classpath=" + macroedPath.toString()); 308 309 List<String> jvmargs = JVM_OPTIONS.fetchFrom(params); 310 int idx = 1; 311 for (String a : jvmargs) { 312 out.println("jvmarg."+idx+"="+a); 313 idx++; 314 } 315 Map<String, String> jvmProps = JVM_PROPERTIES.fetchFrom(params); 316 for (Map.Entry<String, String> entry : jvmProps.entrySet()) { 317 out.println("jvmarg."+idx+"=-D"+entry.getKey()+"="+entry.getValue()); 318 idx++; 319 } 320 321 String preloader = PRELOADER_CLASS.fetchFrom(params); 322 if (preloader != null) { 323 out.println("jvmarg."+idx+"=-Djavafx.preloader="+preloader); 324 } 325 326 //app.id required for setting user preferences (Java Preferences API) 327 out.println("app.preferences.id=" + PREFERENCES_ID.fetchFrom(params)); 328 329 Map<String, String> overridableJVMOptions = USER_JVM_OPTIONS.fetchFrom(params); 330 idx = 1; 331 for (Map.Entry<String, String> arg: overridableJVMOptions.entrySet()) { 332 if (arg.getKey() == null || arg.getValue() == null) { 333 Log.info(I18N.getString("message.jvm-user-arg-is-null")); 334 } 335 else { 336 out.println("jvmuserarg."+idx+".name="+arg.getKey()); 337 out.println("jvmuserarg."+idx+".value="+arg.getValue()); 338 } 339 idx++; 340 } 341 342 // add command line args 343 List<String> args = ARGUMENTS.fetchFrom(params); 344 idx = 1; 345 for (String a : args) { 346 out.println("arg."+idx+"="+a); 347 idx++; 348 } 349 350 out.close(); 351 } 352 353 private void copyRuntime(Map<String, ? super Object> params, File runtimeDirectory) throws IOException { 354 RelativeFileSet runtime = LINUX_RUNTIME.fetchFrom(params); 355 if (runtime == null) { 356 //request to use system runtime 357 return; 358 } 359 runtimeDirectory.mkdirs(); 360 361 File srcdir = runtime.getBaseDirectory(); 362 Set<String> filesToCopy = runtime.getIncludedFiles(); 363 for (String fname : filesToCopy) { 364 IOUtils.copyFile( 365 new File(srcdir, fname), new File(runtimeDirectory, fname)); 366 } 367 } 368 369 @Override 370 public String getName() { 371 return I18N.getString("bundler.name"); 372 } 373 374 @Override 375 public String getDescription() { 376 return I18N.getString("bundler.description"); 377 } 378 379 @Override 380 public String getID() { 381 return "linux.app"; 382 } 383 384 @Override 385 public String getBundleType() { 386 return "IMAGE"; 387 } 388 389 @Override 390 public Collection<BundlerParamInfo<?>> getBundleParameters() { 391 return getAppBundleParameters(); 392 } 393 394 public static Collection<BundlerParamInfo<?>> getAppBundleParameters() { 395 return Arrays.asList( 396 APP_NAME, 397 APP_RESOURCES, 398 // APP_RESOURCES_LIST, // ?? 399 ARGUMENTS, 400 CLASSPATH, 401 JVM_OPTIONS, 402 JVM_PROPERTIES, 403 LINUX_RUNTIME, 404 MAIN_CLASS, 405 MAIN_JAR, 406 PREFERENCES_ID, 407 PRELOADER_CLASS, 408 USER_JVM_OPTIONS, 409 VERSION 410 ); 411 } 412 413 @Override 414 public File execute(Map<String, ? super Object> params, File outputParentDir) { 415 return doBundle(params, outputParentDir, false); 416 } 417 }