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 }