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