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