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)), getRuntimeLocation(p)); 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 String getRuntimeLocation(Map<String, ? super Object> params) { 273 if (LINUX_RUNTIME.fetchFrom(params) == null) { 274 return ""; 275 } else { 276 return "$APPDIR/runtime"; 277 } 278 } 279 280 private void writeCfgFile(Map<String, ? super Object> params, File rootDir) throws FileNotFoundException { 281 File cfgFile = new File(rootDir, getLauncherCfgName(params)); 282 283 cfgFile.delete(); 284 PrintStream out = new PrintStream(cfgFile); 285 out.println("app.runtime=" + getRuntimeLocation(params)); 286 out.println("app.mainjar=" + MAIN_JAR.fetchFrom(params).getIncludedFiles().iterator().next()); 287 out.println("app.version=" + VERSION.fetchFrom(params)); 288 289 //use '/' in the class name (instead of '.' to simplify native code 290 out.println("app.mainclass=" + 291 MAIN_CLASS.fetchFrom(params).replaceAll("\\.", "/")); 292 293 StringBuilder macroedPath = new StringBuilder(); 294 for (String s : CLASSPATH.fetchFrom(params).split("[ ;:]+")) { 295 macroedPath.append(s); 296 macroedPath.append(":"); 297 } 298 macroedPath.deleteCharAt(macroedPath.length() - 1); 299 out.println("app.classpath=" + macroedPath.toString()); 300 301 List<String> jvmargs = JVM_OPTIONS.fetchFrom(params); 302 int idx = 1; 303 for (String a : jvmargs) { 304 out.println("jvmarg."+idx+"="+a); 305 idx++; 306 } 307 Map<String, String> jvmProps = JVM_PROPERTIES.fetchFrom(params); 308 for (Map.Entry<String, String> entry : jvmProps.entrySet()) { 309 out.println("jvmarg."+idx+"=-D"+entry.getKey()+"="+entry.getValue()); 310 idx++; 311 } 312 313 String preloader = PRELOADER_CLASS.fetchFrom(params); 314 if (preloader != null) { 315 out.println("jvmarg."+idx+"=-Djavafx.preloader="+preloader); 316 } 317 318 //app.id required for setting user preferences (Java Preferences API) 319 out.println("app.preferences.id=" + PREFERENCES_ID.fetchFrom(params)); 320 out.println("app.identifier=" + IDENTIFIER.fetchFrom(params)); 321 322 Map<String, String> overridableJVMOptions = USER_JVM_OPTIONS.fetchFrom(params); 323 idx = 1; 324 for (Map.Entry<String, String> arg: overridableJVMOptions.entrySet()) { 325 if (arg.getKey() == null || arg.getValue() == null) { 326 Log.info(I18N.getString("message.jvm-user-arg-is-null")); 327 } 328 else { 329 out.println("jvmuserarg."+idx+".name="+arg.getKey()); 330 out.println("jvmuserarg."+idx+".value="+arg.getValue()); 331 } 332 idx++; 333 } 334 335 // add command line args 336 List<String> args = ARGUMENTS.fetchFrom(params); 337 idx = 1; 338 for (String a : args) { 339 out.println("arg."+idx+"="+a); 340 idx++; 341 } 342 343 out.close(); 344 } 345 346 private void copyRuntime(Map<String, ? super Object> params, File runtimeDirectory) throws IOException { 347 RelativeFileSet runtime = LINUX_RUNTIME.fetchFrom(params); 348 if (runtime == null) { 349 //request to use system runtime 350 return; 351 } 352 runtimeDirectory.mkdirs(); 353 354 File srcdir = runtime.getBaseDirectory(); 355 Set<String> filesToCopy = runtime.getIncludedFiles(); 356 for (String fname : filesToCopy) { 357 IOUtils.copyFile( 358 new File(srcdir, fname), new File(runtimeDirectory, fname)); 359 } 360 } 361 362 @Override 363 public String getName() { 364 return I18N.getString("bundler.name"); 365 } 366 367 @Override 368 public String getDescription() { 369 return I18N.getString("bundler.description"); 370 } 371 372 @Override 373 public String getID() { 374 return "linux.app"; 375 } 376 377 @Override 378 public String getBundleType() { 379 return "IMAGE"; 380 } 381 382 @Override 383 public Collection<BundlerParamInfo<?>> getBundleParameters() { 384 return getAppBundleParameters(); 385 } 386 387 public static Collection<BundlerParamInfo<?>> getAppBundleParameters() { 388 return Arrays.asList( 389 APP_NAME, 390 APP_RESOURCES, 391 // APP_RESOURCES_LIST, // ?? 392 ARGUMENTS, 393 CLASSPATH, 394 JVM_OPTIONS, 395 JVM_PROPERTIES, 396 LINUX_RUNTIME, 397 MAIN_CLASS, 398 MAIN_JAR, 399 PREFERENCES_ID, 400 PRELOADER_CLASS, 401 USER_JVM_OPTIONS, 402 VERSION 403 ); 404 } 405 406 @Override 407 public File execute(Map<String, ? super Object> params, File outputParentDir) { 408 return doBundle(params, outputParentDir, false); 409 } 410 411 @Override 412 protected String getCacheLocation(Map<String, ? super Object> params) { 413 return "$CACHEDIR/"; 414 } 415 416 @Override 417 public void extractRuntimeFlags(Map<String, ? super Object> params) { 418 if (params.containsKey(".runtime.autodetect")) return; 419 420 params.put(".runtime.autodetect", "attempted"); 421 RelativeFileSet runtime = LINUX_RUNTIME.fetchFrom(params); 422 String commandline; 423 if (runtime == null) { 424 //System JRE, report nothing useful 425 params.put(".runtime.autodetect", "systemjre"); 426 } else { 427 File runtimePath = runtime.getBaseDirectory(); 428 File launcherPath = new File(runtimePath, "bin/java"); 429 430 ProcessBuilder pb = new ProcessBuilder(launcherPath.getAbsolutePath(), "-version"); 431 try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { 432 try (PrintStream pout = new PrintStream(baos)) { 433 IOUtils.exec(pb, Log.isDebug(), true, pout); 434 } 435 436 commandline = baos.toString(); 437 } catch (IOException e) { 438 e.printStackTrace(); 439 params.put(".runtime.autodetect", "failed"); 440 return; 441 } 442 AbstractImageBundler.extractFlagsFromVersion(params, commandline); 443 params.put(".runtime.autodetect", "succeeded"); 444 } 445 } 446 }