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 }