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