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.sun.javafx.tools.packager.bundlers; 27 28 import com.oracle.bundlers.AbstractBundler; 29 import com.oracle.bundlers.BundlerParamInfo; 30 import com.oracle.bundlers.StandardBundlerParam; 31 import com.oracle.bundlers.windows.WindowsBundlerParam; 32 import com.sun.javafx.tools.packager.Log; 33 import com.sun.javafx.tools.resource.windows.WinResources; 34 35 import java.io.*; 36 import java.text.MessageFormat; 37 import java.util.*; 38 import java.util.regex.Matcher; 39 import java.util.regex.Pattern; 40 41 import static com.oracle.bundlers.StandardBundlerParam.SERVICE_HINT; 42 import static com.oracle.bundlers.StandardBundlerParam.VERBOSE; 43 import static com.oracle.bundlers.windows.WindowsBundlerParam.*; 44 45 public class WinExeBundler extends AbstractBundler { 46 47 private static final ResourceBundle I18N = 48 ResourceBundle.getBundle("com.oracle.bundlers.windows.WinExeBundler"); 49 50 public static final BundlerParamInfo<WinAppBundler> APP_BUNDLER = new WindowsBundlerParam<>( 51 I18N.getString("param.app-bundler.name"), 52 I18N.getString("param.app-bundler.description"), 53 "win.app.bundler", 54 WinAppBundler.class, 55 params -> new WinAppBundler(), 56 null); 57 58 public static final BundlerParamInfo<WinServiceBundler> SERVICE_BUNDLER = new WindowsBundlerParam<>( 59 I18N.getString("param.service-bundler.name"), 60 I18N.getString("param.service-bundler.description"), 61 "win.service.bundler", 62 WinServiceBundler.class, 63 params -> new WinServiceBundler(), 64 null); 65 66 public static final BundlerParamInfo<File> CONFIG_ROOT = new WindowsBundlerParam<>( 67 I18N.getString("param.config-root.name"), 68 I18N.getString("param.config-root.description"), 69 "configRoot", 70 File.class, params -> { 71 File imagesRoot = new File(BUILD_ROOT.fetchFrom(params), "windows"); 72 imagesRoot.mkdirs(); 73 return imagesRoot; 74 }, 75 (s, p) -> null); 76 77 //default for .exe is user level installation 78 // only do system wide if explicitly requested 79 public static final StandardBundlerParam<Boolean> EXE_SYSTEM_WIDE = 80 new StandardBundlerParam<>( 81 I18N.getString("param.system-wide.name"), 82 I18N.getString("param.system-wide.description"), 83 "win.exe." + BundleParams.PARAM_SYSTEM_WIDE, 84 Boolean.class, 85 params -> params.containsKey(SYSTEM_WIDE.getID()) 86 ? SYSTEM_WIDE.fetchFrom(params) 87 : false, // EXEs default to user local install 88 (s, p) -> (s == null || "null".equalsIgnoreCase(s))? null : Boolean.valueOf(s) // valueOf(null) is false, and we actually do want null 89 ); 90 91 public static final BundlerParamInfo<File> EXE_IMAGE_DIR = new WindowsBundlerParam<>( 92 I18N.getString("param.image-dir.name"), 93 I18N.getString("param.image-dir.description"), 94 "win.exe.imageDir", 95 File.class, 96 params -> { 97 File imagesRoot = IMAGES_ROOT.fetchFrom(params); 98 if (!imagesRoot.exists()) imagesRoot.mkdirs(); 99 return new File(imagesRoot, "win-exe.image"); 100 }, 101 (s, p) -> null); 102 103 private final static String DEFAULT_EXE_PROJECT_TEMPLATE = "template.iss"; 104 private static final String TOOL_INNO_SETUP_COMPILER = "iscc.exe"; 105 106 public static final BundlerParamInfo<String> TOOL_INNO_SETUP_COMPILER_EXECUTABLE = new WindowsBundlerParam<>( 107 I18N.getString("param.iscc-path.name"), 108 I18N.getString("param.iscc-path.description"), 109 "win.exe.iscc.exe", 110 String.class, 111 params -> { 112 for (String dirString : (System.getenv("PATH") + ";C:\\Program Files (x86)\\Inno Setup 5;C:\\Program Files\\Inno Setup 5").split(";")) { 113 File f = new File(dirString.replace("\"", ""), TOOL_INNO_SETUP_COMPILER); 114 if (f.isFile()) { 115 return f.toString(); 116 } 117 } 118 return null; 119 }, 120 null); 121 122 public WinExeBundler() { 123 super(); 124 baseResourceLoader = WinResources.class; 125 } 126 127 @Override 128 public String getName() { 129 return I18N.getString("bundler.name"); 130 } 131 132 @Override 133 public String getDescription() { 134 return I18N.getString("bundler.description"); 135 } 136 137 @Override 138 public String getID() { 139 return "exe"; 140 } 141 142 @Override 143 public String getBundleType() { 144 return "INSTALLER"; 145 } 146 147 @Override 148 public Collection<BundlerParamInfo<?>> getBundleParameters() { 149 Collection<BundlerParamInfo<?>> results = new LinkedHashSet<>(); 150 results.addAll(WinAppBundler.getAppBundleParameters()); 151 results.addAll(getExeBundleParameters()); 152 return results; 153 } 154 155 public static Collection<BundlerParamInfo<?>> getExeBundleParameters() { 156 return Arrays.asList( 157 APP_BUNDLER, 158 APP_RESOURCES, 159 BUILD_ROOT, 160 //CONFIG_ROOT, // duplicate from getAppBundleParameters 161 DESCRIPTION, 162 COPYRIGHT, 163 EXE_SYSTEM_WIDE, 164 IDENTIFIER, 165 EXE_IMAGE_DIR, 166 IMAGES_ROOT, 167 LICENSE_FILE, 168 MENU_GROUP, 169 MENU_HINT, 170 SHORTCUT_HINT, 171 SERVICE_HINT, 172 START_ON_INSTALL, 173 STOP_ON_UNINSTALL, 174 RUN_AT_STARTUP, 175 TITLE, 176 VENDOR, 177 VERSION 178 ); 179 } 180 181 @Override 182 public File execute(Map<String, ? super Object> params, File outputParentDir) { 183 return bundle(params, outputParentDir); 184 } 185 186 static class VersionExtractor extends PrintStream { 187 double version = 0f; 188 189 public VersionExtractor() { 190 super(new ByteArrayOutputStream()); 191 } 192 193 double getVersion() { 194 if (version == 0f) { 195 String content = new String(((ByteArrayOutputStream) out).toByteArray()); 196 Pattern pattern = Pattern.compile("Inno Setup (\\d+.?\\d*)"); 197 Matcher matcher = pattern.matcher(content); 198 if (matcher.find()) { 199 String v = matcher.group(1); 200 version = new Double(v); 201 } 202 } 203 return version; 204 } 205 } 206 207 private static double findToolVersion(String toolName) { 208 try { 209 if (toolName == null || "".equals(toolName)) return 0f; 210 211 ProcessBuilder pb = new ProcessBuilder( 212 toolName, 213 "/?"); 214 VersionExtractor ve = new VersionExtractor(); 215 IOUtils.exec(pb, Log.isDebug(), true, ve); //not interested in the output 216 double version = ve.getVersion(); 217 Log.verbose(MessageFormat.format(I18N.getString("message.tool-version"), toolName, version)); 218 return version; 219 } catch (Exception e) { 220 if (Log.isDebug()) { 221 e.printStackTrace(); 222 } 223 return 0f; 224 } 225 } 226 227 @Override 228 public boolean validate(Map<String, ? super Object> p) throws UnsupportedPlatformException, ConfigException { 229 try { 230 if (p == null) throw new ConfigException(I18N.getString("error.parameters-null"), I18N.getString("error.parameters-null.advice")); 231 232 //run basic validation to ensure requirements are met 233 //we are not interested in return code, only possible exception 234 APP_BUNDLER.fetchFrom(p).validate(p); 235 236 // make sure some key values don't have newlines 237 for (BundlerParamInfo<String> pi : Arrays.asList( 238 APP_NAME, 239 COPYRIGHT, 240 DESCRIPTION, 241 MENU_GROUP, 242 TITLE, 243 VENDOR, 244 VERSION) 245 ) { 246 String v = pi.fetchFrom(p); 247 if (v.contains("\n") | v.contains("\r")) { 248 throw new ConfigException("Parmeter '" + pi.getID() + "' cannot contain a newline.", 249 "Change the value of '" + pi.getID() + " so that it does not contain any newlines"); 250 } 251 } 252 253 //exe bundlers trim the copyright to 100 characters, tell them this will happen 254 if (COPYRIGHT.fetchFrom(p).length() > 100) { 255 throw new ConfigException( 256 I18N.getString("error.copyright-is-too-long"), 257 I18N.getString("error.copyright-is-too-long.advice")); 258 } 259 260 // validate license file, if used, exists in the proper place 261 if (p.containsKey(LICENSE_FILE.getID())) { 262 RelativeFileSet appResources = APP_RESOURCES.fetchFrom(p); 263 for (String license : LICENSE_FILE.fetchFrom(p)) { 264 if (!appResources.contains(license)) { 265 throw new ConfigException( 266 I18N.getString("error.license-missing"), 267 MessageFormat.format(I18N.getString("error.license-missing.advice"), 268 license, appResources.getBaseDirectory().toString())); 269 } 270 } 271 } 272 273 274 if (SERVICE_HINT.fetchFrom(p)) { 275 SERVICE_BUNDLER.fetchFrom(p).validate(p); 276 } 277 278 double innoVersion = findToolVersion(TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(p)); 279 280 //Inno Setup 5+ is required 281 double minVersion = 5.0f; 282 283 if (innoVersion < minVersion) { 284 Log.info(MessageFormat.format(I18N.getString("message.tool-wrong-version"), TOOL_INNO_SETUP_COMPILER, innoVersion, minVersion)); 285 throw new ConfigException( 286 I18N.getString("error.iscc-not-found"), 287 I18N.getString("error.iscc-not-found.advice")); 288 } 289 290 return true; 291 } catch (RuntimeException re) { 292 throw new ConfigException(re); 293 } 294 } 295 296 private boolean prepareProto(Map<String, ? super Object> params) throws IOException { 297 File imageDir = EXE_IMAGE_DIR.fetchFrom(params); 298 File appOutputDir = APP_BUNDLER.fetchFrom(params).doBundle(params, imageDir, true); 299 if (appOutputDir == null) { 300 return false; 301 } 302 List<String> licenseFiles = LICENSE_FILE.fetchFrom(params); 303 if (licenseFiles != null) { 304 RelativeFileSet appRoot = APP_RESOURCES.fetchFrom(params); 305 //need to copy license file to the root of win-app.image 306 for (String s : licenseFiles) { 307 File lfile = new File(appRoot.getBaseDirectory(), s); 308 IOUtils.copyFile(lfile, new File(imageDir, lfile.getName())); 309 } 310 } 311 312 if (SERVICE_HINT.fetchFrom(params)) { 313 // copies the service launcher to the app root folder 314 appOutputDir = SERVICE_BUNDLER.fetchFrom(params).doBundle(params, appOutputDir, true); 315 if (appOutputDir == null) { 316 return false; 317 } 318 } 319 return true; 320 } 321 322 public File bundle(Map<String, ? super Object> p, File outputDirectory) { 323 if (!outputDirectory.isDirectory() && !outputDirectory.mkdirs()) { 324 throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-create-output-dir"), outputDirectory.getAbsolutePath())); 325 } 326 if (!outputDirectory.canWrite()) { 327 throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-write-to-output-dir"), outputDirectory.getAbsolutePath())); 328 } 329 330 // validate we have valid tools before continuing 331 String iscc = TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(p); 332 if (iscc == null || !new File(iscc).isFile()) { 333 Log.info(I18N.getString("error.iscc-not-found")); 334 Log.info(MessageFormat.format(I18N.getString("message.iscc-file-string"), iscc)); 335 return null; 336 } 337 338 File imageDir = EXE_IMAGE_DIR.fetchFrom(p); 339 try { 340 imageDir.mkdirs(); 341 342 boolean menuShortcut = MENU_HINT.fetchFrom(p); 343 boolean desktopShortcut = SHORTCUT_HINT.fetchFrom(p); 344 if (!menuShortcut && !desktopShortcut) { 345 //both can not be false - user will not find the app 346 Log.verbose(I18N.getString("message.one-shortcut-required")); 347 p.put(MENU_HINT.getID(), true); 348 } 349 350 if (prepareProto(p) && prepareProjectConfig(p)) { 351 File configScript = getConfig_Script(p); 352 if (configScript.exists()) { 353 Log.info(MessageFormat.format(I18N.getString("message.running-wsh-script"), configScript.getAbsolutePath())); 354 IOUtils.run("wscript", configScript, VERBOSE.fetchFrom(p)); 355 } 356 return buildEXE(p, outputDirectory); 357 } 358 return null; 359 } catch (IOException ex) { 360 ex.printStackTrace(); 361 return null; 362 } finally { 363 try { 364 if (VERBOSE.fetchFrom(p)) { 365 saveConfigFiles(p); 366 } 367 if (imageDir != null && !Log.isDebug()) { 368 IOUtils.deleteRecursive(imageDir); 369 } else if (imageDir != null) { 370 Log.info(MessageFormat.format(I18N.getString("message.debug-working-directory"), imageDir.getAbsolutePath())); 371 } 372 } catch (FileNotFoundException ex) { 373 //noinspection ReturnInsideFinallyBlock 374 return null; 375 } 376 } 377 } 378 379 //name of post-image script 380 private File getConfig_Script(Map<String, ? super Object> params) { 381 return new File(EXE_IMAGE_DIR.fetchFrom(params), APP_NAME.fetchFrom(params) + "-post-image.wsf"); 382 } 383 384 protected void saveConfigFiles(Map<String, ? super Object> params) { 385 try { 386 File configRoot = CONFIG_ROOT.fetchFrom(params); 387 if (getConfig_ExeProjectFile(params).exists()) { 388 IOUtils.copyFile(getConfig_ExeProjectFile(params), 389 new File(configRoot, getConfig_ExeProjectFile(params).getName())); 390 } 391 if (getConfig_Script(params).exists()) { 392 IOUtils.copyFile(getConfig_Script(params), 393 new File(configRoot, getConfig_Script(params).getName())); 394 } 395 if (getConfig_SmallInnoSetupIcon(params).exists()) { 396 IOUtils.copyFile(getConfig_SmallInnoSetupIcon(params), 397 new File(configRoot, getConfig_SmallInnoSetupIcon(params).getName())); 398 } 399 Log.info(MessageFormat.format(I18N.getString("message.config-save-location"), configRoot.getAbsolutePath())); 400 } catch (IOException ioe) { 401 ioe.printStackTrace(); 402 } 403 } 404 405 private String getAppIdentifier(Map<String, ? super Object> params) { 406 String nm = IDENTIFIER.fetchFrom(params); 407 408 //limitation of innosetup 409 if (nm.length() > 126) 410 nm = nm.substring(0, 126); 411 412 return nm; 413 } 414 415 416 private String getLicenseFile(Map<String, ? super Object> params) { 417 List<String> licenseFiles = LICENSE_FILE.fetchFrom(params); 418 if (licenseFiles == null || licenseFiles.isEmpty()) { 419 return ""; 420 } else { 421 return licenseFiles.get(0); 422 } 423 } 424 425 void validateValueAndPut(Map<String, String> data, String key, BundlerParamInfo<String> param, Map<String, ? super Object> params) throws IOException { 426 String value = param.fetchFrom(params); 427 if (value.contains("\r") || value.contains("\n")) { 428 throw new IOException("Configuration Parameter " + param.getID() + " cannot contain multiple lines of text"); 429 } 430 data.put(key, innosetupEscape(value)); 431 } 432 433 private String innosetupEscape(String value) { 434 if (value.contains("\"") || !value.trim().equals(value)) { 435 value = "\"" + value.replace("\"", "\"\"") + "\""; 436 } 437 return value; 438 } 439 440 boolean prepareMainProjectFile(Map<String, ? super Object> params) throws IOException { 441 Map<String, String> data = new HashMap<>(); 442 data.put("PRODUCT_APP_IDENTIFIER", innosetupEscape(getAppIdentifier(params))); 443 444 validateValueAndPut(data, "APPLICATION_NAME", APP_NAME, params); 445 446 validateValueAndPut(data, "APPLICATION_VENDOR", VENDOR, params); 447 validateValueAndPut(data, "APPLICATION_VERSION", VERSION, params); // TODO make our own version paraminfo? 448 449 data.put("APPLICATION_LAUNCHER_FILENAME", 450 innosetupEscape(WinAppBundler.getLauncher(EXE_IMAGE_DIR.fetchFrom(params), params).getName())); 451 452 data.put("APPLICATION_DESKTOP_SHORTCUT", SHORTCUT_HINT.fetchFrom(params) ? "returnTrue" : "returnFalse"); 453 data.put("APPLICATION_MENU_SHORTCUT", MENU_HINT.fetchFrom(params) ? "returnTrue" : "returnFalse"); 454 validateValueAndPut(data, "APPLICATION_GROUP", MENU_GROUP, params); 455 validateValueAndPut(data, "APPLICATION_COMMENTS", TITLE, params); // TODO this seems strange, at least in name 456 validateValueAndPut(data, "APPLICATION_COPYRIGHT", COPYRIGHT, params); 457 458 data.put("APPLICATION_LICENSE_FILE", innosetupEscape(getLicenseFile(params))); 459 460 if (EXE_SYSTEM_WIDE.fetchFrom(params)) { 461 data.put("APPLICATION_INSTALL_ROOT", "{pf}"); 462 data.put("APPLICATION_INSTALL_PRIVILEGE", "admin"); 463 } else { 464 data.put("APPLICATION_INSTALL_ROOT", "{localappdata}"); 465 data.put("APPLICATION_INSTALL_PRIVILEGE", "lowest"); 466 } 467 468 if (BIT_ARCH_64.fetchFrom(params)) { 469 data.put("ARCHITECTURE_BIT_MODE", "x64"); 470 } else { 471 data.put("ARCHITECTURE_BIT_MODE", ""); 472 } 473 474 if (SERVICE_HINT.fetchFrom(params)) { 475 data.put("RUN_FILENAME", innosetupEscape(WinServiceBundler.getAppSvcName(params))); 476 } else { 477 validateValueAndPut(data, "RUN_FILENAME", APP_NAME, params); 478 } 479 validateValueAndPut(data, "APPLICATION_DESCRIPTION", DESCRIPTION, params); 480 data.put("APPLICATION_SERVICE", SERVICE_HINT.fetchFrom(params) ? "returnTrue" : "returnFalse"); 481 data.put("APPLICATION_NOT_SERVICE", SERVICE_HINT.fetchFrom(params) ? "returnFalse" : "returnTrue"); 482 data.put("START_ON_INSTALL", START_ON_INSTALL.fetchFrom(params) ? "-startOnInstall" : ""); 483 data.put("STOP_ON_UNINSTALL", STOP_ON_UNINSTALL.fetchFrom(params) ? "-stopOnUninstall" : ""); 484 data.put("RUN_AT_STARTUP", RUN_AT_STARTUP.fetchFrom(params) ? "-runAtStartup" : ""); 485 486 Writer w = new BufferedWriter(new FileWriter(getConfig_ExeProjectFile(params))); 487 String content = preprocessTextResource( 488 WinAppBundler.WIN_BUNDLER_PREFIX + getConfig_ExeProjectFile(params).getName(), 489 I18N.getString("resource.inno-setup-project-file"), DEFAULT_EXE_PROJECT_TEMPLATE, data, 490 VERBOSE.fetchFrom(params)); 491 w.write(content); 492 w.close(); 493 return true; 494 } 495 496 private final static String DEFAULT_INNO_SETUP_ICON = "icon_inno_setup.bmp"; 497 498 private boolean prepareProjectConfig(Map<String, ? super Object> params) throws IOException { 499 prepareMainProjectFile(params); 500 501 //prepare installer icon 502 File iconTarget = getConfig_SmallInnoSetupIcon(params); 503 fetchResource(WinAppBundler.WIN_BUNDLER_PREFIX + iconTarget.getName(), 504 I18N.getString("resource.setup-icon"), 505 DEFAULT_INNO_SETUP_ICON, 506 iconTarget, 507 VERBOSE.fetchFrom(params)); 508 509 fetchResource(WinAppBundler.WIN_BUNDLER_PREFIX + getConfig_Script(params).getName(), 510 I18N.getString("resource.post-install-script"), 511 (String) null, 512 getConfig_Script(params), 513 VERBOSE.fetchFrom(params)); 514 return true; 515 } 516 517 private File getConfig_SmallInnoSetupIcon(Map<String, ? super Object> params) { 518 return new File(EXE_IMAGE_DIR.fetchFrom(params), 519 APP_NAME.fetchFrom(params) + "-setup-icon.bmp"); 520 } 521 522 private File getConfig_ExeProjectFile(Map<String, ? super Object> params) { 523 return new File(EXE_IMAGE_DIR.fetchFrom(params), 524 APP_NAME.fetchFrom(params) + ".iss"); 525 } 526 527 528 private File buildEXE(Map<String, ? super Object> params, File outdir) throws IOException { 529 Log.verbose(MessageFormat.format(I18N.getString("message.outputting-to-location"), outdir.getAbsolutePath())); 530 531 outdir.mkdirs(); 532 533 //run candle 534 ProcessBuilder pb = new ProcessBuilder( 535 TOOL_INNO_SETUP_COMPILER_EXECUTABLE.fetchFrom(params), 536 "/o"+outdir.getAbsolutePath(), 537 getConfig_ExeProjectFile(params).getAbsolutePath()); 538 pb = pb.directory(EXE_IMAGE_DIR.fetchFrom(params)); 539 IOUtils.exec(pb, VERBOSE.fetchFrom(params)); 540 541 Log.info(MessageFormat.format(I18N.getString("message.output-location"), outdir.getAbsolutePath())); 542 543 // presume the result is the ".exe" file with the newest modified time 544 // not the best solution, but it is the most reliable 545 File result = null; 546 long lastModified = 0; 547 File[] list = outdir.listFiles(); 548 if (list != null) { 549 for (File f : list) { 550 if (f.getName().endsWith(".exe") && f.lastModified() > lastModified) { 551 result = f; 552 lastModified = f.lastModified(); 553 } 554 } 555 } 556 557 return result; 558 } 559 }