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 }