1 /* 2 * Copyright (c) 2019, 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 jdk.incubator.jpackage.internal; 27 28 import java.io.IOException; 29 import java.nio.charset.StandardCharsets; 30 import java.nio.file.Path; 31 import java.text.MessageFormat; 32 import java.util.*; 33 import java.util.function.*; 34 import java.util.stream.Collectors; 35 import java.util.stream.Stream; 36 import javax.xml.stream.XMLStreamException; 37 import javax.xml.stream.XMLStreamWriter; 38 import jdk.incubator.jpackage.internal.IOUtils.XmlConsumer; 39 import static jdk.incubator.jpackage.internal.StandardBundlerParam.*; 40 import static jdk.incubator.jpackage.internal.WinMsiBundler.*; 41 import static jdk.incubator.jpackage.internal.WindowsBundlerParam.MENU_GROUP; 42 import static jdk.incubator.jpackage.internal.WindowsBundlerParam.WINDOWS_INSTALL_DIR; 43 44 /** 45 * Creates application WiX source files. 46 */ 47 class WixSourcesBuilder { 48 49 WixSourcesBuilder setWixVersion(DottedVersion v) { 50 wixVersion = v; 51 return this; 52 } 53 54 WixSourcesBuilder initFromParams(Path appImageRoot, 55 Map<String, ? super Object> params) { 56 Supplier<ApplicationLayout> appImageSupplier = () -> { 57 if (StandardBundlerParam.isRuntimeInstaller(params)) { 58 return ApplicationLayout.javaRuntime(); 59 } else { 60 return ApplicationLayout.platformAppImage(); 61 } 62 }; 63 64 systemWide = MSI_SYSTEM_WIDE.fetchFrom(params); 65 66 registryKeyPath = Path.of("Software", 67 VENDOR.fetchFrom(params), 68 APP_NAME.fetchFrom(params), 69 VERSION.fetchFrom(params)).toString(); 70 71 installDir = (systemWide ? PROGRAM_FILES : LOCAL_PROGRAM_FILES).resolve( 72 WINDOWS_INSTALL_DIR.fetchFrom(params)); 73 74 do { 75 ApplicationLayout layout = appImageSupplier.get(); 76 // Don't want AppImageFile.FILENAME in installed application. 77 // Register it with app image at a role without a match in installed 78 // app layout to exclude it from layout transformation. 79 layout.pathGroup().setPath(new Object(), 80 AppImageFile.getPathInAppImage(Path.of(""))); 81 82 // Want absolute paths to source files in generated WiX sources. 83 // This is to handle scenario if sources would be processed from 84 // differnt current directory. 85 appImage = layout.resolveAt(appImageRoot.toAbsolutePath().normalize()); 86 } while (false); 87 88 installedAppImage = appImageSupplier.get().resolveAt(INSTALLDIR); 89 90 shortcutFolders = new HashSet<>(); 91 if (SHORTCUT_HINT.fetchFrom(params)) { 92 shortcutFolders.add(ShortcutsFolder.Desktop); 93 } 94 if (MENU_HINT.fetchFrom(params)) { 95 shortcutFolders.add(ShortcutsFolder.ProgramMenu); 96 } 97 98 if (StandardBundlerParam.isRuntimeInstaller(params)) { 99 launcherPaths = Collections.emptyList(); 100 } else { 101 launcherPaths = AppImageFile.getLauncherNames(appImageRoot, params).stream() 102 .map(name -> installedAppImage.launchersDirectory().resolve(name)) 103 .map(WixSourcesBuilder::addExeSuffixToPath) 104 .collect(Collectors.toList()); 105 } 106 107 programMenuFolderName = MENU_GROUP.fetchFrom(params); 108 109 initFileAssociations(params); 110 111 return this; 112 } 113 114 void createMainFragment(Path file) throws IOException { 115 removeFolderItems = new HashMap<>(); 116 defaultedMimes = new HashSet<>(); 117 IOUtils.createXml(file, xml -> { 118 xml.writeStartElement("Wix"); 119 xml.writeDefaultNamespace("http://schemas.microsoft.com/wix/2006/wi"); 120 xml.writeNamespace("util", 121 "http://schemas.microsoft.com/wix/UtilExtension"); 122 123 xml.writeStartElement("Fragment"); 124 125 addFaComponentGroup(xml); 126 127 addShortcutComponentGroup(xml); 128 129 addFilesComponentGroup(xml); 130 131 xml.writeEndElement(); // <Fragment> 132 133 addIconsFragment(xml); 134 135 xml.writeEndElement(); // <Wix> 136 }); 137 } 138 139 void logWixFeatures() { 140 if (wixVersion.compareTo("3.6") >= 0) { 141 Log.verbose(MessageFormat.format(I18N.getString( 142 "message.use-wix36-features"), wixVersion)); 143 } 144 } 145 146 private void normalizeFileAssociation(FileAssociation fa) { 147 fa.launcherPath = addExeSuffixToPath( 148 installedAppImage.launchersDirectory().resolve(fa.launcherPath)); 149 150 if (fa.iconPath != null && !fa.iconPath.toFile().exists()) { 151 fa.iconPath = null; 152 } 153 154 if (fa.iconPath != null) { 155 fa.iconPath = fa.iconPath.toAbsolutePath(); 156 } 157 158 // Filter out empty extensions. 159 fa.extensions = fa.extensions.stream().filter(Predicate.not( 160 String::isEmpty)).collect(Collectors.toList()); 161 } 162 163 private static Path addExeSuffixToPath(Path path) { 164 return IOUtils.addSuffix(path, ".exe"); 165 } 166 167 private Path getInstalledFaIcoPath(FileAssociation fa) { 168 String fname = String.format("fa_%s.ico", String.join("_", fa.extensions)); 169 return installedAppImage.destktopIntegrationDirectory().resolve(fname); 170 } 171 172 private void initFileAssociations(Map<String, ? super Object> params) { 173 associations = FileAssociation.fetchFrom(params).stream() 174 .peek(this::normalizeFileAssociation) 175 // Filter out file associations without extensions. 176 .filter(fa -> !fa.extensions.isEmpty()) 177 .collect(Collectors.toList()); 178 179 associations.stream().filter(fa -> fa.iconPath != null).forEach(fa -> { 180 // Need to add fa icon in the image. 181 Object key = new Object(); 182 appImage.pathGroup().setPath(key, fa.iconPath); 183 installedAppImage.pathGroup().setPath(key, getInstalledFaIcoPath(fa)); 184 }); 185 } 186 187 private static UUID createNameUUID(String str) { 188 return UUID.nameUUIDFromBytes(str.getBytes(StandardCharsets.UTF_8)); 189 } 190 191 private static UUID createNameUUID(Path path, String role) { 192 if (path.isAbsolute() || !ROOT_DIRS.contains(path.getName(0))) { 193 throw throwInvalidPathException(path); 194 } 195 // Paths are case insensitive on Windows 196 String keyPath = path.toString().toLowerCase(); 197 if (role != null) { 198 keyPath = role + "@" + keyPath; 199 } 200 return createNameUUID(keyPath); 201 } 202 203 /** 204 * Value for Id attribute of various WiX elements. 205 */ 206 enum Id { 207 File, 208 Folder("dir"), 209 Shortcut, 210 ProgId, 211 Icon, 212 CreateFolder("mkdir"), 213 RemoveFolder("rm"); 214 215 Id() { 216 this.prefix = name().toLowerCase(); 217 } 218 219 Id(String prefix) { 220 this.prefix = prefix; 221 } 222 223 String of(Path path) { 224 if (this == Folder && KNOWN_DIRS.contains(path)) { 225 return path.getFileName().toString(); 226 } 227 228 String result = of(path, prefix, name()); 229 230 if (this == Icon) { 231 // Icon id constructed from UUID value is too long and triggers 232 // CNDL1000 warning, so use Java hash code instead. 233 result = String.format("%s%d", prefix, result.hashCode()).replace( 234 "-", "_"); 235 } 236 237 return result; 238 } 239 240 private static String of(Path path, String prefix, String role) { 241 Objects.requireNonNull(role); 242 Objects.requireNonNull(prefix); 243 return String.format("%s%s", prefix, 244 createNameUUID(path, role).toString().replace("-", "")); 245 } 246 247 static String of(Path path, String prefix) { 248 return of(path, prefix, prefix); 249 } 250 251 private final String prefix; 252 } 253 254 enum Component { 255 File(cfg().file()), 256 Shortcut(cfg().file().withRegistryKeyPath()), 257 ProgId(cfg().file().withRegistryKeyPath()), 258 CreateFolder(cfg().withRegistryKeyPath()), 259 RemoveFolder(cfg().withRegistryKeyPath()); 260 261 Component() { 262 this.cfg = cfg(); 263 this.id = Id.valueOf(name()); 264 } 265 266 Component(Config cfg) { 267 this.cfg = cfg; 268 this.id = Id.valueOf(name()); 269 } 270 271 UUID guidOf(Path path) { 272 return createNameUUID(path, name()); 273 } 274 275 String idOf(Path path) { 276 return id.of(path); 277 } 278 279 boolean isRegistryKeyPath() { 280 return cfg.withRegistryKeyPath; 281 } 282 283 boolean isFile() { 284 return cfg.isFile; 285 } 286 287 static void startElement(XMLStreamWriter xml, String componentId, 288 String componentGuid) throws XMLStreamException, IOException { 289 xml.writeStartElement("Component"); 290 xml.writeAttribute("Win64", "yes"); 291 xml.writeAttribute("Id", componentId); 292 xml.writeAttribute("Guid", componentGuid); 293 } 294 295 private static final class Config { 296 Config withRegistryKeyPath() { 297 withRegistryKeyPath = true; 298 return this; 299 } 300 301 Config file() { 302 isFile = true; 303 return this; 304 } 305 306 private boolean isFile; 307 private boolean withRegistryKeyPath; 308 } 309 310 private static Config cfg() { 311 return new Config(); 312 } 313 314 private final Config cfg; 315 private final Id id; 316 }; 317 318 private static void addComponentGroup(XMLStreamWriter xml, String id, 319 List<String> componentIds) throws XMLStreamException, IOException { 320 xml.writeStartElement("ComponentGroup"); 321 xml.writeAttribute("Id", id); 322 componentIds = componentIds.stream().filter(Objects::nonNull).collect( 323 Collectors.toList()); 324 for (var componentId : componentIds) { 325 xml.writeStartElement("ComponentRef"); 326 xml.writeAttribute("Id", componentId); 327 xml.writeEndElement(); 328 } 329 xml.writeEndElement(); 330 } 331 332 private String addComponent(XMLStreamWriter xml, Path path, 333 Component role, XmlConsumer xmlConsumer) throws XMLStreamException, 334 IOException { 335 336 final Path directoryRefPath; 337 if (role.isFile()) { 338 directoryRefPath = path.getParent(); 339 } else { 340 directoryRefPath = path; 341 } 342 343 xml.writeStartElement("DirectoryRef"); 344 xml.writeAttribute("Id", Id.Folder.of(directoryRefPath)); 345 346 final String componentId = "c" + role.idOf(path); 347 Component.startElement(xml, componentId, String.format("{%s}", 348 role.guidOf(path))); 349 350 boolean isRegistryKeyPath = !systemWide || role.isRegistryKeyPath(); 351 if (isRegistryKeyPath) { 352 addRegistryKeyPath(xml, directoryRefPath); 353 if ((role.isFile() || (role == Component.CreateFolder 354 && !systemWide)) && !SYSTEM_DIRS.contains(directoryRefPath)) { 355 xml.writeStartElement("RemoveFolder"); 356 int counter = Optional.ofNullable(removeFolderItems.get( 357 directoryRefPath)).orElse(Integer.valueOf(0)).intValue() + 1; 358 removeFolderItems.put(directoryRefPath, counter); 359 xml.writeAttribute("Id", String.format("%s_%d", Id.RemoveFolder.of( 360 directoryRefPath), counter)); 361 xml.writeAttribute("On", "uninstall"); 362 xml.writeEndElement(); 363 } 364 } 365 366 xml.writeStartElement(role.name()); 367 if (role != Component.CreateFolder) { 368 xml.writeAttribute("Id", role.idOf(path)); 369 } 370 371 if (!isRegistryKeyPath) { 372 xml.writeAttribute("KeyPath", "yes"); 373 } 374 375 xmlConsumer.accept(xml); 376 xml.writeEndElement(); 377 378 xml.writeEndElement(); // <Component> 379 xml.writeEndElement(); // <DirectoryRef> 380 381 return componentId; 382 } 383 384 private void addFaComponentGroup(XMLStreamWriter xml) 385 throws XMLStreamException, IOException { 386 387 List<String> componentIds = new ArrayList<>(); 388 for (var fa : associations) { 389 componentIds.addAll(addFaComponents(xml, fa)); 390 } 391 addComponentGroup(xml, "FileAssociations", componentIds); 392 } 393 394 private void addShortcutComponentGroup(XMLStreamWriter xml) throws 395 XMLStreamException, IOException { 396 List<String> componentIds = new ArrayList<>(); 397 Set<ShortcutsFolder> defineShortcutFolders = new HashSet<>(); 398 for (var launcherPath : launcherPaths) { 399 for (var folder : shortcutFolders) { 400 String componentId = addShortcutComponent(xml, launcherPath, 401 folder); 402 if (componentId != null) { 403 defineShortcutFolders.add(folder); 404 componentIds.add(componentId); 405 } 406 } 407 } 408 409 for (var folder : defineShortcutFolders) { 410 Path path = folder.getPath(this); 411 componentIds.addAll(addRootBranch(xml, path)); 412 } 413 414 addComponentGroup(xml, "Shortcuts", componentIds); 415 } 416 417 private String addShortcutComponent(XMLStreamWriter xml, Path launcherPath, 418 ShortcutsFolder folder) throws XMLStreamException, IOException { 419 Objects.requireNonNull(folder); 420 421 if (!INSTALLDIR.equals(launcherPath.getName(0))) { 422 throw throwInvalidPathException(launcherPath); 423 } 424 425 String launcherBasename = IOUtils.replaceSuffix( 426 launcherPath.getFileName(), "").toString(); 427 428 Path shortcutPath = folder.getPath(this).resolve(launcherBasename); 429 return addComponent(xml, shortcutPath, Component.Shortcut, unused -> { 430 final Path icoFile = IOUtils.addSuffix( 431 installedAppImage.destktopIntegrationDirectory().resolve( 432 launcherBasename), ".ico"); 433 434 xml.writeAttribute("Name", launcherBasename); 435 xml.writeAttribute("WorkingDirectory", INSTALLDIR.toString()); 436 xml.writeAttribute("Advertise", "no"); 437 xml.writeAttribute("IconIndex", "0"); 438 xml.writeAttribute("Target", String.format("[#%s]", 439 Component.File.idOf(launcherPath))); 440 xml.writeAttribute("Icon", Id.Icon.of(icoFile)); 441 }); 442 } 443 444 private List<String> addFaComponents(XMLStreamWriter xml, 445 FileAssociation fa) throws XMLStreamException, IOException { 446 List<String> components = new ArrayList<>(); 447 for (var extension: fa.extensions) { 448 Path path = INSTALLDIR.resolve(String.format("%s_%s", extension, 449 fa.launcherPath.getFileName())); 450 components.add(addComponent(xml, path, Component.ProgId, unused -> { 451 xml.writeAttribute("Description", fa.description); 452 453 if (fa.iconPath != null) { 454 xml.writeAttribute("Icon", Id.File.of(getInstalledFaIcoPath( 455 fa))); 456 xml.writeAttribute("IconIndex", "0"); 457 } 458 459 xml.writeStartElement("Extension"); 460 xml.writeAttribute("Id", extension); 461 xml.writeAttribute("Advertise", "no"); 462 463 var mimeIt = fa.mimeTypes.iterator(); 464 if (mimeIt.hasNext()) { 465 String mime = mimeIt.next(); 466 xml.writeAttribute("ContentType", mime); 467 468 if (!defaultedMimes.contains(mime)) { 469 xml.writeStartElement("MIME"); 470 xml.writeAttribute("ContentType", mime); 471 xml.writeAttribute("Default", "yes"); 472 xml.writeEndElement(); 473 defaultedMimes.add(mime); 474 } 475 } 476 477 xml.writeStartElement("Verb"); 478 xml.writeAttribute("Id", "open"); 479 xml.writeAttribute("Command", "Open"); 480 xml.writeAttribute("Argument", "%1"); 481 xml.writeAttribute("TargetFile", Id.File.of(fa.launcherPath)); 482 xml.writeEndElement(); // <Verb> 483 484 xml.writeEndElement(); // <Extension> 485 })); 486 } 487 488 return components; 489 } 490 491 private List<String> addRootBranch(XMLStreamWriter xml, Path path) 492 throws XMLStreamException, IOException { 493 if (!ROOT_DIRS.contains(path.getName(0))) { 494 throw throwInvalidPathException(path); 495 } 496 497 Function<Path, String> createDirectoryName = dir -> null; 498 499 boolean sysDir = true; 500 int levels = 1; 501 var dirIt = path.iterator(); 502 xml.writeStartElement("DirectoryRef"); 503 xml.writeAttribute("Id", dirIt.next().toString()); 504 505 path = path.getName(0); 506 while (dirIt.hasNext()) { 507 levels++; 508 Path name = dirIt.next(); 509 path = path.resolve(name); 510 511 if (sysDir && !SYSTEM_DIRS.contains(path)) { 512 sysDir = false; 513 createDirectoryName = dir -> dir.getFileName().toString(); 514 } 515 516 final String directoryId; 517 if (!sysDir && path.equals(installDir)) { 518 directoryId = INSTALLDIR.toString(); 519 } else { 520 directoryId = Id.Folder.of(path); 521 } 522 xml.writeStartElement("Directory"); 523 xml.writeAttribute("Id", directoryId); 524 525 String directoryName = createDirectoryName.apply(path); 526 if (directoryName != null) { 527 xml.writeAttribute("Name", directoryName); 528 } 529 } 530 531 while (0 != levels--) { 532 xml.writeEndElement(); 533 } 534 535 List<String> componentIds = new ArrayList<>(); 536 while (!SYSTEM_DIRS.contains(path = path.getParent())) { 537 componentIds.add(addRemoveDirectoryComponent(xml, path)); 538 } 539 540 return componentIds; 541 } 542 543 private String addRemoveDirectoryComponent(XMLStreamWriter xml, Path path) 544 throws XMLStreamException, IOException { 545 return addComponent(xml, path, Component.RemoveFolder, 546 unused -> xml.writeAttribute("On", "uninstall")); 547 } 548 549 private List<String> addDirectoryHierarchy(XMLStreamWriter xml) 550 throws XMLStreamException, IOException { 551 552 Set<Path> allDirs = new HashSet<>(); 553 Set<Path> emptyDirs = new HashSet<>(); 554 appImage.transform(installedAppImage, new PathGroup.TransformHandler() { 555 @Override 556 public void copyFile(Path src, Path dst) throws IOException { 557 Path dir = dst.getParent(); 558 createDirectory(dir); 559 emptyDirs.remove(dir); 560 } 561 562 @Override 563 public void createDirectory(final Path dir) throws IOException { 564 if (!allDirs.contains(dir)) { 565 emptyDirs.add(dir); 566 } 567 568 Path it = dir; 569 while (it != null && allDirs.add(it)) { 570 it = it.getParent(); 571 } 572 573 it = dir; 574 while ((it = it.getParent()) != null && emptyDirs.remove(it)); 575 } 576 }); 577 578 List<String> componentIds = new ArrayList<>(); 579 for (var dir : emptyDirs) { 580 componentIds.add(addComponent(xml, dir, Component.CreateFolder, 581 unused -> {})); 582 } 583 584 if (!systemWide) { 585 // Per-user install requires <RemoveFolder> component in every 586 // directory. 587 for (var dir : allDirs.stream() 588 .filter(Predicate.not(emptyDirs::contains)) 589 .filter(Predicate.not(removeFolderItems::containsKey)) 590 .collect(Collectors.toList())) { 591 componentIds.add(addRemoveDirectoryComponent(xml, dir)); 592 } 593 } 594 595 allDirs.remove(INSTALLDIR); 596 for (var dir : allDirs) { 597 xml.writeStartElement("DirectoryRef"); 598 xml.writeAttribute("Id", Id.Folder.of(dir.getParent())); 599 xml.writeStartElement("Directory"); 600 xml.writeAttribute("Id", Id.Folder.of(dir)); 601 xml.writeAttribute("Name", dir.getFileName().toString()); 602 xml.writeEndElement(); 603 xml.writeEndElement(); 604 } 605 606 componentIds.addAll(addRootBranch(xml, installDir)); 607 608 return componentIds; 609 } 610 611 private void addFilesComponentGroup(XMLStreamWriter xml) 612 throws XMLStreamException, IOException { 613 614 List<Map.Entry<Path, Path>> files = new ArrayList<>(); 615 appImage.transform(installedAppImage, new PathGroup.TransformHandler() { 616 @Override 617 public void copyFile(Path src, Path dst) throws IOException { 618 files.add(Map.entry(src, dst)); 619 } 620 621 @Override 622 public void createDirectory(final Path dir) throws IOException { 623 } 624 }); 625 626 List<String> componentIds = new ArrayList<>(); 627 for (var file : files) { 628 Path src = file.getKey(); 629 Path dst = file.getValue(); 630 631 componentIds.add(addComponent(xml, dst, Component.File, unused -> { 632 xml.writeAttribute("Source", src.normalize().toString()); 633 Path name = dst.getFileName(); 634 if (!name.equals(src.getFileName())) { 635 xml.writeAttribute("Name", name.toString()); 636 } 637 })); 638 } 639 640 componentIds.addAll(addDirectoryHierarchy(xml)); 641 642 componentIds.add(addDirectoryCleaner(xml, INSTALLDIR)); 643 644 addComponentGroup(xml, "Files", componentIds); 645 } 646 647 private void addIconsFragment(XMLStreamWriter xml) throws 648 XMLStreamException, IOException { 649 650 PathGroup srcPathGroup = appImage.pathGroup(); 651 PathGroup dstPathGroup = installedAppImage.pathGroup(); 652 653 // Build list of copy operations for all .ico files in application image 654 List<Map.Entry<Path, Path>> icoFiles = new ArrayList<>(); 655 srcPathGroup.transform(dstPathGroup, new PathGroup.TransformHandler() { 656 @Override 657 public void copyFile(Path src, Path dst) throws IOException { 658 if (src.getFileName().toString().endsWith(".ico")) { 659 icoFiles.add(Map.entry(src, dst)); 660 } 661 } 662 663 @Override 664 public void createDirectory(Path dst) throws IOException { 665 } 666 }); 667 668 xml.writeStartElement("Fragment"); 669 for (var icoFile : icoFiles) { 670 xml.writeStartElement("Icon"); 671 xml.writeAttribute("Id", Id.Icon.of(icoFile.getValue())); 672 xml.writeAttribute("SourceFile", icoFile.getKey().toString()); 673 xml.writeEndElement(); 674 } 675 xml.writeEndElement(); 676 } 677 678 private void addRegistryKeyPath(XMLStreamWriter xml, Path path) throws 679 XMLStreamException, IOException { 680 addRegistryKeyPath(xml, path, () -> "ProductCode", () -> "[ProductCode]"); 681 } 682 683 private void addRegistryKeyPath(XMLStreamWriter xml, Path path, 684 Supplier<String> nameAttr, Supplier<String> valueAttr) throws 685 XMLStreamException, IOException { 686 687 String regRoot = USER_PROFILE_DIRS.stream().anyMatch(path::startsWith) 688 || !systemWide ? "HKCU" : "HKLM"; 689 690 xml.writeStartElement("RegistryKey"); 691 xml.writeAttribute("Root", regRoot); 692 xml.writeAttribute("Key", registryKeyPath); 693 if (wixVersion.compareTo("3.6") < 0) { 694 xml.writeAttribute("Action", "createAndRemoveOnUninstall"); 695 } 696 xml.writeStartElement("RegistryValue"); 697 xml.writeAttribute("Type", "string"); 698 xml.writeAttribute("KeyPath", "yes"); 699 xml.writeAttribute("Name", nameAttr.get()); 700 xml.writeAttribute("Value", valueAttr.get()); 701 xml.writeEndElement(); // <RegistryValue> 702 xml.writeEndElement(); // <RegistryKey> 703 } 704 705 private String addDirectoryCleaner(XMLStreamWriter xml, Path path) throws 706 XMLStreamException, IOException { 707 if (wixVersion.compareTo("3.6") < 0) { 708 return null; 709 } 710 711 // rm -rf 712 final String baseId = Id.of(path, "rm_rf"); 713 final String propertyId = baseId.toUpperCase(); 714 final String componentId = ("c" + baseId); 715 716 xml.writeStartElement("Property"); 717 xml.writeAttribute("Id", propertyId); 718 xml.writeStartElement("RegistrySearch"); 719 xml.writeAttribute("Id", Id.of(path, "regsearch")); 720 xml.writeAttribute("Root", systemWide ? "HKLM" : "HKCU"); 721 xml.writeAttribute("Key", registryKeyPath); 722 xml.writeAttribute("Type", "raw"); 723 xml.writeAttribute("Name", propertyId); 724 xml.writeEndElement(); // <RegistrySearch> 725 xml.writeEndElement(); // <Property> 726 727 xml.writeStartElement("DirectoryRef"); 728 xml.writeAttribute("Id", INSTALLDIR.toString()); 729 Component.startElement(xml, componentId, "*"); 730 731 addRegistryKeyPath(xml, INSTALLDIR, () -> propertyId, () -> { 732 // The following code converts a path to value to be saved in registry. 733 // E.g.: 734 // INSTALLDIR -> [INSTALLDIR] 735 // TERGETDIR/ProgramFiles64Folder/foo/bar -> [ProgramFiles64Folder]foo/bar 736 final Path rootDir = KNOWN_DIRS.stream() 737 .sorted(Comparator.comparing(Path::getNameCount).reversed()) 738 .filter(path::startsWith) 739 .findFirst().get(); 740 StringBuilder sb = new StringBuilder(); 741 sb.append(String.format("[%s]", rootDir.getFileName().toString())); 742 sb.append(rootDir.relativize(path).toString()); 743 return sb.toString(); 744 }); 745 746 xml.writeStartElement( 747 "http://schemas.microsoft.com/wix/UtilExtension", 748 "RemoveFolderEx"); 749 xml.writeAttribute("On", "uninstall"); 750 xml.writeAttribute("Property", propertyId); 751 xml.writeEndElement(); // <RemoveFolderEx> 752 xml.writeEndElement(); // <Component> 753 xml.writeEndElement(); // <DirectoryRef> 754 755 return componentId; 756 } 757 758 private static IllegalArgumentException throwInvalidPathException(Path v) { 759 throw new IllegalArgumentException(String.format("Invalid path [%s]", v)); 760 } 761 762 enum ShortcutsFolder { 763 ProgramMenu(PROGRAM_MENU_PATH), 764 Desktop(DESKTOP_PATH); 765 766 private ShortcutsFolder(Path root) { 767 this.root = root; 768 } 769 770 Path getPath(WixSourcesBuilder outer) { 771 if (this == ProgramMenu) { 772 return root.resolve(outer.programMenuFolderName); 773 } 774 return root; 775 } 776 777 private final Path root; 778 } 779 780 private DottedVersion wixVersion; 781 782 private boolean systemWide; 783 784 private String registryKeyPath; 785 786 private Path installDir; 787 788 private String programMenuFolderName; 789 790 private List<FileAssociation> associations; 791 792 private Set<ShortcutsFolder> shortcutFolders; 793 794 private List<Path> launcherPaths; 795 796 private ApplicationLayout appImage; 797 private ApplicationLayout installedAppImage; 798 799 private Map<Path, Integer> removeFolderItems; 800 private Set<String> defaultedMimes; 801 802 private final static Path TARGETDIR = Path.of("TARGETDIR"); 803 804 private final static Path INSTALLDIR = Path.of("INSTALLDIR"); 805 806 private final static Set<Path> ROOT_DIRS = Set.of(INSTALLDIR, TARGETDIR); 807 808 private final static Path PROGRAM_MENU_PATH = TARGETDIR.resolve("ProgramMenuFolder"); 809 810 private final static Path DESKTOP_PATH = TARGETDIR.resolve("DesktopFolder"); 811 812 private final static Path PROGRAM_FILES = TARGETDIR.resolve("ProgramFiles64Folder"); 813 814 private final static Path LOCAL_PROGRAM_FILES = TARGETDIR.resolve("LocalAppDataFolder"); 815 816 private final static Set<Path> SYSTEM_DIRS = Set.of(TARGETDIR, 817 PROGRAM_MENU_PATH, DESKTOP_PATH, PROGRAM_FILES, LOCAL_PROGRAM_FILES); 818 819 private final static Set<Path> KNOWN_DIRS = Stream.of(Set.of(INSTALLDIR), 820 SYSTEM_DIRS).flatMap(Set::stream).collect( 821 Collectors.toUnmodifiableSet()); 822 823 private final static Set<Path> USER_PROFILE_DIRS = Set.of(LOCAL_PROGRAM_FILES, 824 PROGRAM_MENU_PATH, DESKTOP_PATH); 825 826 private static final StandardBundlerParam<Boolean> MENU_HINT = 827 new WindowsBundlerParam<>( 828 Arguments.CLIOptions.WIN_MENU_HINT.getId(), 829 Boolean.class, 830 params -> false, 831 // valueOf(null) is false, 832 // and we actually do want null in some cases 833 (s, p) -> (s == null || 834 "null".equalsIgnoreCase(s))? true : Boolean.valueOf(s) 835 ); 836 837 private static final StandardBundlerParam<Boolean> SHORTCUT_HINT = 838 new WindowsBundlerParam<>( 839 Arguments.CLIOptions.WIN_SHORTCUT_HINT.getId(), 840 Boolean.class, 841 params -> false, 842 // valueOf(null) is false, 843 // and we actually do want null in some cases 844 (s, p) -> (s == null || 845 "null".equalsIgnoreCase(s))? false : Boolean.valueOf(s) 846 ); 847 }