소스 검색

CPack/WIX: Add support for WiX Toolset v4

Add a `CPACK_WIX_VERSION` option to specify version WiX for
which the project is configured.

Fixes: #23910
Brad King 1 년 전
부모
커밋
03884f4f32

+ 3 - 0
.gitlab/ci/configure_windows_wix_common.cmake

@@ -1,2 +1,5 @@
 get_filename_component(wix3_dir "${CMAKE_CURRENT_LIST_DIR}/../wix3" ABSOLUTE)
 set(CMake_TEST_CPACK_WIX3 "${wix3_dir}" CACHE PATH "")
+
+get_filename_component(wix4_dir "${CMAKE_CURRENT_LIST_DIR}/../wix4" ABSOLUTE)
+set(CMake_TEST_CPACK_WIX4 "${wix4_dir}" CACHE PATH "")

+ 1 - 0
.gitlab/ci/env_windows_arm64_vs2022_ninja.ps1

@@ -1 +1,2 @@
 & "$pwsh" -File .gitlab/ci/wix3.ps1
+& "$pwsh" -File .gitlab/ci/wix4.ps1

+ 1 - 0
.gitlab/ci/env_windows_vs2022_x64_ninja.ps1

@@ -5,3 +5,4 @@ if ("$env:CMAKE_CI_NIGHTLY" -eq "true") {
 }
 
 & "$pwsh" -File .gitlab/ci/wix3.ps1
+& "$pwsh" -File .gitlab/ci/wix4.ps1

+ 90 - 9
Help/cpack_gen/wix.rst

@@ -9,8 +9,68 @@ Use the `WiX Toolset`_ to produce a Windows Installer ``.msi`` database.
   The :variable:`CPACK_COMPONENT_<compName>_DISABLED` variable is now
   supported.
 
+WiX Toolsets
+^^^^^^^^^^^^
+
+CPack selects one of the following variants of the WiX Toolset
+based on the :variable:`CPACK_WIX_VERSION` variable:
+
+* `WiX .NET Tools`_
+* `WiX Toolset v3`_
+
+WiX .NET Tools
+""""""""""""""
+
+Packaging is performed using the following tools:
+
+``wix build``
+  Build WiX source files directly into a Windows Installer ``.msi`` database.
+
+  Invocations may be customized using tool-specific variables:
+
+  * :variable:`CPACK_WIX_BUILD_EXTENSIONS <CPACK_WIX_<TOOL>_EXTENSIONS>`
+  * :variable:`CPACK_WIX_BUILD_EXTRA_FLAGS <CPACK_WIX_<TOOL>_EXTRA_FLAGS>`
+
+WiX extensions must be named with the form ``WixToolset.<Name>.wixext``.
+
+CPack expects the ``wix`` .NET tool to be available for command-line use
+with any required WiX extensions already installed.  Be sure the ``wix``
+version is compatible with :variable:`CPACK_WIX_VERSION`, and that WiX
+extension versions match the ``wix`` tool version.  For example:
+
+1. Install the ``wix`` command-line tool using ``dotnet``.
+
+  To install ``wix`` globally for the current user:
+
+  .. code-block:: bat
+
+    dotnet tool install --global wix --version 4.0.4
+
+  This places ``wix.exe`` in ``%USERPROFILE%\.dotnet\tools`` and adds
+  the directory to the current user's ``PATH`` environment variable.
+
+  Or, to install ``wix`` in a specific path, e.g., in ``c:\WiX``:
+
+  .. code-block:: bat
+
+    dotnet tool install --tool-path c:\WiX wix --version 4.0.4
+
+  This places ``wix.exe`` in ``c:\WiX``, but does *not* add it to the
+  current user's ``PATH`` environment variable.  The ``WIX`` environment
+  variable may be set to tell CPack where to find the tool,
+  e.g., ``set WIX=c:\WiX``.
+
+2. Add the WiX ``UI`` extension, needed by CPack's default WiX template:
+
+  .. code-block:: bat
+
+    wix extension add --global WixToolset.UI.wixext/4.0.4
+
+  Extensions added globally are stored in ``%USERPROFILE%\.wix``, or if the
+  ``WIX_EXTENSIONS`` environment variable is set, in ``%WIX_EXTENSIONS%\.wix``.
+
 WiX Toolset v3
-^^^^^^^^^^^^^^
+""""""""""""""
 
 Packaging is performed using the following tools:
 
@@ -45,6 +105,19 @@ Variables specific to CPack WIX generator
 The following variables are specific to the installers built on
 Windows using WiX.
 
+.. variable:: CPACK_WIX_VERSION
+
+ .. versionadded:: 3.30
+
+ Specify the version of WiX Toolset for which the configuration
+ is written.  The value must be one of
+
+ ``4``
+   Package using `WiX .NET Tools`_.
+
+ ``3``
+   Package using `WiX Toolset v3`_.  This is the default.
+
 .. variable:: CPACK_WIX_UPGRADE_GUID
 
  Upgrade GUID (``Product/@UpgradeCode``)
@@ -101,8 +174,13 @@ Windows using WiX.
 
 .. variable:: CPACK_WIX_UI_REF
 
- Specify the WiX ``UI`` extension's dialog set.
- This is the Id of the ``<UIRef>`` element in the default WiX template.
+ Specify the WiX ``UI`` extension's dialog set:
+
+ * With `WiX .NET Tools`_, this is the Id of the
+   ``<ui:WixUI>`` element in the default WiX template.
+
+ * With `WiX Toolset v3`_, this is the Id of the
+   ``<UIRef>`` element in the default WiX template.
 
  The default is ``WixUI_InstallDir`` in case no CPack components have
  been defined and ``WixUI_FeatureTree`` otherwise.
@@ -234,7 +312,7 @@ Windows using WiX.
 
 .. variable:: CPACK_WIX_EXTRA_OBJECTS
 
- Extra WiX object files or libraries.
+ Extra WiX object files or libraries to use with `WiX Toolset v3`_.
 
  This variable provides an optional list of extra WiX object (``.wixobj``)
  and/or WiX library (``.wixlib``) files.  The paths must be absolute.
@@ -242,17 +320,17 @@ Windows using WiX.
 .. variable:: CPACK_WIX_EXTENSIONS
 
  Specify a list of additional extensions for WiX tools.
- See `WiX Toolset v3`_ for extension naming patterns.
+ See `WiX Toolsets`_ for extension naming patterns.
 
 .. variable:: CPACK_WIX_<TOOL>_EXTENSIONS
 
  Specify a list of additional extensions for a specific WiX tool.
- See `WiX Toolset v3`_ for possible ``<TOOL>`` names.
+ See `WiX Toolsets`_ for possible ``<TOOL>`` names.
 
 .. variable:: CPACK_WIX_<TOOL>_EXTRA_FLAGS
 
  Specify a list of additional command-line flags for a specific WiX tool.
- See `WiX Toolset v3`_ for possible ``<TOOL>`` names.
+ See `WiX Toolsets`_ for possible ``<TOOL>`` names.
 
  Use it at your own risk.
  Future versions of CPack may generate flags which may be in conflict
@@ -356,8 +434,8 @@ Windows using WiX.
  .. versionadded:: 3.23
 
  If this variable is set to true, the default inclusion of the WiX ``UI``
- extension is skipped, i.e., the ``-ext WixUIExtension`` flag is not
- passed to WiX tools.
+ extension is skipped, i.e., the ``-ext WixUIExtension`` or
+ ``-ext WixToolset.UI.wixext`` flag is not passed to WiX tools.
 
 .. variable:: CPACK_WIX_ARCHITECTURE
 
@@ -386,6 +464,9 @@ Windows using WiX.
  ``NONE``
    Create an installer without any ``InstallScope`` attribute.
 
+   This is not supported if :variable:`CPACK_WIX_VERSION` is set
+   to any value other than ``3``.
+
    .. deprecated:: 3.29
 
      This value is only for compatibility with the inconsistent behavior used

+ 5 - 0
Help/release/dev/cpack-wix.rst

@@ -0,0 +1,5 @@
+cpack-wix
+---------
+
+* The :cpack_gen:`CPack WIX Generator` gained support for WiX Toolset v4.
+  See the :variable:`CPACK_WIX_VERSION` variable.

+ 17 - 11
Modules/Internal/CPack/CPackWIX.cmake

@@ -5,18 +5,24 @@ if(NOT CPACK_WIX_ROOT)
   string(REPLACE "\\" "/" CPACK_WIX_ROOT "$ENV{WIX}")
 endif()
 
-find_program(CPACK_WIX_CANDLE_EXECUTABLE candle
-  PATHS "${CPACK_WIX_ROOT}" PATH_SUFFIXES "bin")
+if(CPACK_WIX_VERSION VERSION_GREATER_EQUAL 4)
+  find_program(CPACK_WIX_EXECUTABLE NAMES wix
+    PATHS "${CPACK_WIX_ROOT}" PATH_SUFFIXES "bin")
+  if(NOT CPACK_WIX_EXECUTABLE)
+    message(FATAL_ERROR "Could not find the 'wix' executable.")
+  endif()
+else()
+  find_program(CPACK_WIX_CANDLE_EXECUTABLE candle
+    PATHS "${CPACK_WIX_ROOT}" PATH_SUFFIXES "bin")
+  if(NOT CPACK_WIX_CANDLE_EXECUTABLE)
+    message(FATAL_ERROR "Could not find the WiX candle executable.")
+  endif()
 
-if(NOT CPACK_WIX_CANDLE_EXECUTABLE)
-  message(FATAL_ERROR "Could not find the WiX candle executable.")
-endif()
-
-find_program(CPACK_WIX_LIGHT_EXECUTABLE light
-  PATHS "${CPACK_WIX_ROOT}" PATH_SUFFIXES "bin")
-
-if(NOT CPACK_WIX_LIGHT_EXECUTABLE)
-  message(FATAL_ERROR "Could not find the WiX light executable.")
+  find_program(CPACK_WIX_LIGHT_EXECUTABLE light
+    PATHS "${CPACK_WIX_ROOT}" PATH_SUFFIXES "bin")
+  if(NOT CPACK_WIX_LIGHT_EXECUTABLE)
+    message(FATAL_ERROR "Could not find the WiX light executable.")
+  endif()
 endif()
 
 if(NOT DEFINED CPACK_WIX_INSTALL_SCOPE)

+ 53 - 0
Modules/Internal/CPack/WIX.template.in

@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?include "cpack_variables.wxi"?>
+
+<Wix
+    xmlns="http://wixtoolset.org/schemas/v4/wxs"@CPACK_WIX_CUSTOM_XMLNS_EXPANDED@
+    RequiredVersion="4.0"
+    >
+
+    <Package
+        Name="$(var.CPACK_PACKAGE_NAME)"
+        Version="$(var.CPACK_PACKAGE_VERSION)"
+        Manufacturer="$(var.CPACK_PACKAGE_VENDOR)"
+        UpgradeCode="$(var.CPACK_WIX_UPGRADE_GUID)"
+        ProductCode="$(var.CPACK_WIX_PRODUCT_GUID)"
+        Scope="$(var.CPACK_WIX_INSTALL_SCOPE)"
+        InstallerVersion="500"
+        Language="1033"
+        Compressed="yes"
+        >
+
+        <Media Id="1" Cabinet="media1.cab" EmbedCab="yes"/>
+
+        <MajorUpgrade
+            Schedule="afterInstallInitialize"
+            AllowSameVersionUpgrades="yes"
+            DowngradeErrorMessage="A later version of [ProductName] is already installed. Setup will now exit."/>
+
+        <WixVariable Id="WixUILicenseRtf" Value="$(var.CPACK_WIX_LICENSE_RTF)"/>
+        <Property Id="WIXUI_INSTALLDIR" Value="INSTALL_ROOT"/>
+
+        <?ifdef CPACK_WIX_PRODUCT_ICON?>
+        <Property Id="ARPPRODUCTICON" Value="ProductIcon.ico" />
+        <Icon Id="ProductIcon.ico" SourceFile="$(var.CPACK_WIX_PRODUCT_ICON)"/>
+        <?endif?>
+
+        <?ifdef CPACK_WIX_UI_BANNER?>
+        <WixVariable Id="WixUIBannerBmp" Value="$(var.CPACK_WIX_UI_BANNER)"/>
+        <?endif?>
+
+        <?ifdef CPACK_WIX_UI_DIALOG?>
+        <WixVariable Id="WixUIDialogBmp" Value="$(var.CPACK_WIX_UI_DIALOG)"/>
+        <?endif?>
+
+        <FeatureRef Id="ProductFeature"/>
+
+        <ui:WixUI Id="$(var.CPACK_WIX_UI_REF)" />
+        <UIRef Id="WixUI_ErrorProgressText" />
+
+        <?include "properties.wxi"?>
+        <?include "product_fragment.wxi"?>
+    </Package>
+</Wix>

+ 128 - 29
Source/CPack/WiX/cmCPackWIXGenerator.cxx

@@ -166,6 +166,16 @@ int cmCPackWIXGenerator::PackageFiles()
 
 bool cmCPackWIXGenerator::InitializeWiXConfiguration()
 {
+  if (cmValue wixVersion = GetOption("CPACK_WIX_VERSION")) {
+    if (!cmStrToULong(*wixVersion, &this->WixVersion) ||
+        this->WixVersion < 3 || this->WixVersion > 4) {
+      cmCPackLogger(cmCPackLog::LOG_ERROR,
+                    "CPACK_WIX_VERSION has unknown value '"
+                      << *wixVersion << "'" << std::endl);
+      return false;
+    }
+  }
+
   if (!ReadListFile("Internal/CPack/CPackWIX.cmake")) {
     cmCPackLogger(cmCPackLog::LOG_ERROR,
                   "Error while executing CPackWIX.cmake" << std::endl);
@@ -232,14 +242,22 @@ bool cmCPackWIXGenerator::InitializeWiXConfiguration()
     SetOption("CPACK_WIX_PROPERTY_ARPCONTACT", packageContact);
   }
 
-  CollectExtensions("CPACK_WIX_EXTENSIONS", this->CandleExtensions);
-  CollectExtensions("CPACK_WIX_CANDLE_EXTENSIONS", this->CandleExtensions);
+  if (this->WixVersion >= 4) {
+    CollectExtensions("CPACK_WIX_EXTENSIONS", this->WixExtensions);
+    if (!GetOption("CPACK_WIX_SKIP_WIX_UI_EXTENSION").IsOn()) {
+      this->WixExtensions.insert("WixToolset.UI.wixext");
+    }
+  } else {
+    CollectExtensions("CPACK_WIX_EXTENSIONS", this->CandleExtensions);
+    CollectExtensions("CPACK_WIX_CANDLE_EXTENSIONS", this->CandleExtensions);
 
-  if (!GetOption("CPACK_WIX_SKIP_WIX_UI_EXTENSION").IsOn()) {
-    this->LightExtensions.insert("WixUIExtension");
+    if (!GetOption("CPACK_WIX_SKIP_WIX_UI_EXTENSION").IsOn()) {
+      this->LightExtensions.insert("WixUIExtension");
+    }
+    CollectExtensions("CPACK_WIX_EXTENSIONS", this->LightExtensions);
+    CollectExtensions("CPACK_WIX_LIGHT_EXTENSIONS", this->LightExtensions);
   }
-  CollectExtensions("CPACK_WIX_EXTENSIONS", this->LightExtensions);
-  CollectExtensions("CPACK_WIX_LIGHT_EXTENSIONS", this->LightExtensions);
+
   CollectXmlNamespaces("CPACK_WIX_CUSTOM_XMLNS", this->CustomXmlNamespaces);
 
   cmValue patchFilePath = GetOption("CPACK_WIX_PATCH_FILE");
@@ -278,6 +296,53 @@ bool cmCPackWIXGenerator::PackageFilesImpl()
 
   AppendUserSuppliedExtraSources();
 
+  return this->WixVersion >= 4 ? this->PackageWithWix()
+                               : this->PackageWithWix3();
+}
+
+bool cmCPackWIXGenerator::PackageWithWix()
+{
+  std::string wixExecutable;
+  if (!RequireOption("CPACK_WIX_EXECUTABLE", wixExecutable)) {
+    return false;
+  }
+
+  std::string arch;
+  if (cmValue archOpt = GetOption("CPACK_WIX_ARCHITECTURE")) {
+    arch = *archOpt;
+  } else {
+    arch = GetArchitecture();
+    cmCPackLogger(
+      cmCPackLog::LOG_VERBOSE,
+      "CPACK_WIX_ARCHITECTURE was not set. Invoking WiX with architecture "
+        << arch << ". " << std::endl);
+  }
+
+  std::ostringstream command;
+  command << QuotePath(wixExecutable) << " build"
+          << " -arch " << arch << " -out "
+          << QuotePath(CMakeToWixPath(packageFileNames.at(0)));
+
+  for (std::string const& ext : this->WixExtensions) {
+    command << " -ext " << QuotePath(ext);
+  }
+
+  cmList cultures{ GetOption("CPACK_WIX_CULTURES") };
+  for (std::string const& culture : cultures) {
+    command << " -culture \"" << culture << "\"";
+  }
+
+  AddCustomFlags("CPACK_WIX_BUILD_EXTRA_FLAGS", command);
+
+  for (std::string const& sourceFilename : this->WixSources) {
+    command << " -src " << QuotePath(CMakeToWixPath(sourceFilename));
+  }
+
+  return RunWiXCommand(command.str());
+}
+
+bool cmCPackWIXGenerator::PackageWithWix3()
+{
   std::set<std::string> usedBaseNames;
 
   std::ostringstream objectFiles;
@@ -341,8 +406,8 @@ void cmCPackWIXGenerator::CreateWiXVariablesIncludeFile()
   std::string includeFilename =
     cmStrCat(this->CPackTopLevel, "/cpack_variables.wxi");
 
-  cmWIXSourceWriter includeFile(this->Logger, includeFilename,
-                                this->ComponentGuidType,
+  cmWIXSourceWriter includeFile(this->WixVersion, this->Logger,
+                                includeFilename, this->ComponentGuidType,
                                 cmWIXSourceWriter::INCLUDE_ELEMENT_ROOT);
   InjectXmlNamespaces(includeFile);
 
@@ -367,8 +432,8 @@ void cmCPackWIXGenerator::CreateWiXPropertiesIncludeFile()
   std::string includeFilename =
     cmStrCat(this->CPackTopLevel, "/properties.wxi");
 
-  cmWIXSourceWriter includeFile(this->Logger, includeFilename,
-                                this->ComponentGuidType,
+  cmWIXSourceWriter includeFile(this->WixVersion, this->Logger,
+                                includeFilename, this->ComponentGuidType,
                                 cmWIXSourceWriter::INCLUDE_ELEMENT_ROOT);
   InjectXmlNamespaces(includeFile);
 
@@ -417,8 +482,8 @@ void cmCPackWIXGenerator::CreateWiXProductFragmentIncludeFile()
   std::string includeFilename =
     cmStrCat(this->CPackTopLevel, "/product_fragment.wxi");
 
-  cmWIXSourceWriter includeFile(this->Logger, includeFilename,
-                                this->ComponentGuidType,
+  cmWIXSourceWriter includeFile(this->WixVersion, this->Logger,
+                                includeFilename, this->ComponentGuidType,
                                 cmWIXSourceWriter::INCLUDE_ELEMENT_ROOT);
   InjectXmlNamespaces(includeFile);
 
@@ -459,7 +524,8 @@ bool cmCPackWIXGenerator::CreateWiXSourceFiles()
   this->WixSources.push_back(directoryDefinitionsFilename);
 
   cmWIXDirectoriesSourceWriter directoryDefinitions(
-    this->Logger, directoryDefinitionsFilename, this->ComponentGuidType);
+    this->WixVersion, this->Logger, directoryDefinitionsFilename,
+    this->ComponentGuidType);
   InjectXmlNamespaces(directoryDefinitions);
   directoryDefinitions.BeginElement("Fragment");
 
@@ -468,11 +534,13 @@ bool cmCPackWIXGenerator::CreateWiXSourceFiles()
     return false;
   }
 
-  directoryDefinitions.BeginElement("Directory");
-  directoryDefinitions.AddAttribute("Id", "TARGETDIR");
-  directoryDefinitions.AddAttribute("Name", "SourceDir");
+  if (this->WixVersion == 3) {
+    directoryDefinitions.BeginElement("Directory");
+    directoryDefinitions.AddAttribute("Id", "TARGETDIR");
+    directoryDefinitions.AddAttribute("Name", "SourceDir");
+  }
 
-  size_t installRootSize =
+  auto installationPrefixDirectory =
     directoryDefinitions.BeginInstallationPrefixDirectory(GetRootFolderId(),
                                                           installRoot);
 
@@ -481,7 +549,8 @@ bool cmCPackWIXGenerator::CreateWiXSourceFiles()
 
   this->WixSources.push_back(fileDefinitionsFilename);
 
-  cmWIXFilesSourceWriter fileDefinitions(this->Logger, fileDefinitionsFilename,
+  cmWIXFilesSourceWriter fileDefinitions(this->WixVersion, this->Logger,
+                                         fileDefinitionsFilename,
                                          this->ComponentGuidType);
   InjectXmlNamespaces(fileDefinitions);
 
@@ -492,8 +561,9 @@ bool cmCPackWIXGenerator::CreateWiXSourceFiles()
 
   this->WixSources.push_back(featureDefinitionsFilename);
 
-  cmWIXFeaturesSourceWriter featureDefinitions(
-    this->Logger, featureDefinitionsFilename, this->ComponentGuidType);
+  cmWIXFeaturesSourceWriter featureDefinitions(this->WixVersion, this->Logger,
+                                               featureDefinitionsFilename,
+                                               this->ComponentGuidType);
   InjectXmlNamespaces(featureDefinitions);
 
   featureDefinitions.BeginElement("Fragment");
@@ -501,7 +571,11 @@ bool cmCPackWIXGenerator::CreateWiXSourceFiles()
   featureDefinitions.BeginElement("Feature");
   featureDefinitions.AddAttribute("Id", "ProductFeature");
   featureDefinitions.AddAttribute("Display", "expand");
-  featureDefinitions.AddAttribute("Absent", "disallow");
+  if (this->WixVersion >= 4) {
+    featureDefinitions.AddAttribute("AllowAbsent", "no");
+  } else {
+    featureDefinitions.AddAttribute("Absent", "disallow");
+  }
   featureDefinitions.AddAttribute("ConfigurableDirectory", "INSTALL_ROOT");
 
   std::string cpackPackageName;
@@ -583,7 +657,8 @@ bool cmCPackWIXGenerator::CreateWiXSourceFiles()
   featureDefinitions.EndElement("Fragment");
   fileDefinitions.EndElement("Fragment");
 
-  directoryDefinitions.EndInstallationPrefixDirectory(installRootSize);
+  directoryDefinitions.EndInstallationPrefixDirectory(
+    installationPrefixDirectory);
 
   if (emittedShortcutTypes.find(cmWIXShortcuts::START_MENU) !=
       emittedShortcutTypes.end()) {
@@ -601,7 +676,9 @@ bool cmCPackWIXGenerator::CreateWiXSourceFiles()
     directoryDefinitions.EmitStartupFolder();
   }
 
-  directoryDefinitions.EndElement("Directory");
+  if (this->WixVersion == 3) {
+    directoryDefinitions.EndElement("Directory");
+  }
   directoryDefinitions.EndElement("Fragment");
 
   if (!GenerateMainSourceFileFromTemplate()) {
@@ -613,15 +690,19 @@ bool cmCPackWIXGenerator::CreateWiXSourceFiles()
 
 std::string cmCPackWIXGenerator::GetRootFolderId() const
 {
+  std::string result;
+
   if (GetOption("CPACK_WIX_SKIP_PROGRAM_FOLDER").IsOn()) {
-    return "";
+    return result;
   }
 
-  std::string result = "ProgramFiles<64>Folder";
-
   cmValue rootFolderId = GetOption("CPACK_WIX_ROOT_FOLDER_ID");
   if (rootFolderId) {
     result = *rootFolderId;
+  } else if (this->WixVersion >= 4) {
+    result = "ProgramFiles6432Folder";
+  } else {
+    result = "ProgramFiles<64>Folder";
   }
 
   if (GetArchitecture() == "x86"_s) {
@@ -640,7 +721,9 @@ bool cmCPackWIXGenerator::GenerateMainSourceFileFromTemplate()
     wixTemplate = *wixtpl;
   } else {
     cm::optional<cm::string_view> alt;
-    alt = "WIX-v3/"_s;
+    if (this->WixVersion == 3) {
+      alt = "WIX-v3/"_s;
+    }
     wixTemplate = FindTemplate("WIX.template.in"_s, alt);
   }
 
@@ -768,21 +851,31 @@ bool cmCPackWIXGenerator::CreateShortcutsOfSpecificType(
   cmWIXFeaturesSourceWriter& featureDefinitions)
 {
   std::string directoryId;
+  std::string directoryRef = "DirectoryRef";
   switch (type) {
     case cmWIXShortcuts::START_MENU: {
       cmValue cpackWixProgramMenuFolder =
         GetOption("CPACK_WIX_PROGRAM_MENU_FOLDER");
       if (cpackWixProgramMenuFolder && cpackWixProgramMenuFolder == "."_s) {
         directoryId = "ProgramMenuFolder";
+        if (this->WixVersion >= 4) {
+          directoryRef = "StandardDirectory";
+        }
       } else {
         directoryId = "PROGRAM_MENU_FOLDER";
       }
     } break;
     case cmWIXShortcuts::DESKTOP:
       directoryId = "DesktopFolder";
+      if (this->WixVersion >= 4) {
+        directoryRef = "StandardDirectory";
+      }
       break;
     case cmWIXShortcuts::STARTUP:
       directoryId = "StartupFolder";
+      if (this->WixVersion >= 4) {
+        directoryRef = "StandardDirectory";
+      }
       break;
     default:
       return false;
@@ -814,7 +907,7 @@ bool cmCPackWIXGenerator::CreateShortcutsOfSpecificType(
 
   componentId += idSuffix;
 
-  fileDefinitions.BeginElement("DirectoryRef");
+  fileDefinitions.BeginElement(directoryRef);
   fileDefinitions.AddAttribute("Id", directoryId);
 
   fileDefinitions.BeginElement("Component");
@@ -844,7 +937,7 @@ bool cmCPackWIXGenerator::CreateShortcutsOfSpecificType(
   }
 
   fileDefinitions.EndElement("Component");
-  fileDefinitions.EndElement("DirectoryRef");
+  fileDefinitions.EndElement(directoryRef);
 
   featureDefinitions.EmitComponentRef(componentId);
   featureDefinitions.EndElement("FeatureRef");
@@ -1199,6 +1292,12 @@ void cmCPackWIXGenerator::CollectXmlNamespaces(std::string const& variableName,
     }
   }
   std::string xmlns;
+  if (this->WixVersion >= 4 &&
+      cm::contains(this->WixExtensions, "WixToolset.UI.wixext") &&
+      !cm::contains(namespaces, "ui")) {
+    xmlns = cmStrCat(
+      xmlns, "\n    xmlns:ui=\"http://wixtoolset.org/schemas/v4/wxs/ui\"");
+  }
   for (auto& ns : namespaces) {
     xmlns = cmStrCat(xmlns, "\n    xmlns:", ns.first, "=\"",
                      cmWIXSourceWriter::EscapeAttributeValue(ns.second), '"');

+ 5 - 0
Source/CPack/WiX/cmCPackWIXGenerator.h

@@ -59,6 +59,8 @@ private:
   bool InitializeWiXConfiguration();
 
   bool PackageFilesImpl();
+  bool PackageWithWix();
+  bool PackageWithWix3();
 
   void CreateWiXVariablesIncludeFile();
 
@@ -160,6 +162,7 @@ private:
   id_map_t PathToIdMap;
   ambiguity_map_t IdAmbiguityCounter;
 
+  extension_set_t WixExtensions;
   extension_set_t CandleExtensions;
   extension_set_t LightExtensions;
   xmlns_map_t CustomXmlNamespaces;
@@ -168,5 +171,7 @@ private:
 
   std::unique_ptr<cmWIXPatch> Patch;
 
+  unsigned long WixVersion = 3;
+
   cmWIXSourceWriter::GuidType ComponentGuidType;
 };

+ 28 - 17
Source/CPack/WiX/cmWIXDirectoriesSourceWriter.cxx

@@ -5,15 +5,16 @@
 #include <cmext/string_view>
 
 cmWIXDirectoriesSourceWriter::cmWIXDirectoriesSourceWriter(
-  cmCPackLog* logger, std::string const& filename, GuidType componentGuidType)
-  : cmWIXSourceWriter(logger, filename, componentGuidType)
+  unsigned long wixVersion, cmCPackLog* logger, std::string const& filename,
+  GuidType componentGuidType)
+  : cmWIXSourceWriter(wixVersion, logger, filename, componentGuidType)
 {
 }
 
 void cmWIXDirectoriesSourceWriter::EmitStartMenuFolder(
   std::string const& startMenuFolder)
 {
-  BeginElement("Directory");
+  BeginElement_StandardDirectory();
   AddAttribute("Id", "ProgramMenuFolder");
 
   if (startMenuFolder != "."_s) {
@@ -23,34 +24,39 @@ void cmWIXDirectoriesSourceWriter::EmitStartMenuFolder(
     EndElement("Directory");
   }
 
-  EndElement("Directory");
+  EndElement_StandardDirectory();
 }
 
 void cmWIXDirectoriesSourceWriter::EmitDesktopFolder()
 {
-  BeginElement("Directory");
+  BeginElement_StandardDirectory();
   AddAttribute("Id", "DesktopFolder");
-  AddAttribute("Name", "Desktop");
-  EndElement("Directory");
+  if (this->WixVersion == 3) {
+    AddAttribute("Name", "Desktop");
+  }
+  EndElement_StandardDirectory();
 }
 
 void cmWIXDirectoriesSourceWriter::EmitStartupFolder()
 {
-  BeginElement("Directory");
+  BeginElement_StandardDirectory();
   AddAttribute("Id", "StartupFolder");
-  AddAttribute("Name", "Startup");
-  EndElement("Directory");
+  if (this->WixVersion == 3) {
+    AddAttribute("Name", "Startup");
+  }
+  EndElement_StandardDirectory();
 }
 
-size_t cmWIXDirectoriesSourceWriter::BeginInstallationPrefixDirectory(
+cmWIXDirectoriesSourceWriter::InstallationPrefixDirectory
+cmWIXDirectoriesSourceWriter::BeginInstallationPrefixDirectory(
   std::string const& programFilesFolderId,
   std::string const& installRootString)
 {
-  size_t offset = 1;
+  InstallationPrefixDirectory installationPrefixDirectory;
   if (!programFilesFolderId.empty()) {
-    BeginElement("Directory");
+    installationPrefixDirectory.HasStandardDirectory = true;
+    this->BeginElement_StandardDirectory();
     AddAttribute("Id", programFilesFolderId);
-    offset = 0;
   }
 
   std::vector<std::string> installRoot;
@@ -62,6 +68,7 @@ size_t cmWIXDirectoriesSourceWriter::BeginInstallationPrefixDirectory(
   }
 
   for (size_t i = 1; i < installRoot.size(); ++i) {
+    ++installationPrefixDirectory.Depth;
     BeginElement("Directory");
 
     if (i == installRoot.size() - 1) {
@@ -75,12 +82,16 @@ size_t cmWIXDirectoriesSourceWriter::BeginInstallationPrefixDirectory(
     AddAttribute("Name", installRoot[i]);
   }
 
-  return installRoot.size() - offset;
+  return installationPrefixDirectory;
 }
 
-void cmWIXDirectoriesSourceWriter::EndInstallationPrefixDirectory(size_t size)
+void cmWIXDirectoriesSourceWriter::EndInstallationPrefixDirectory(
+  InstallationPrefixDirectory installationPrefixDirectory)
 {
-  for (size_t i = 0; i < size; ++i) {
+  for (size_t i = 0; i < installationPrefixDirectory.Depth; ++i) {
     EndElement("Directory");
   }
+  if (installationPrefixDirectory.HasStandardDirectory) {
+    this->EndElement_StandardDirectory();
+  }
 }

+ 11 - 3
Source/CPack/WiX/cmWIXDirectoriesSourceWriter.h

@@ -13,7 +13,8 @@
 class cmWIXDirectoriesSourceWriter : public cmWIXSourceWriter
 {
 public:
-  cmWIXDirectoriesSourceWriter(cmCPackLog* logger, std::string const& filename,
+  cmWIXDirectoriesSourceWriter(unsigned long wixVersion, cmCPackLog* logger,
+                               std::string const& filename,
                                GuidType componentGuidType);
 
   void EmitStartMenuFolder(std::string const& startMenuFolder);
@@ -22,9 +23,16 @@ public:
 
   void EmitStartupFolder();
 
-  size_t BeginInstallationPrefixDirectory(
+  struct InstallationPrefixDirectory
+  {
+    bool HasStandardDirectory = false;
+    size_t Depth = 0;
+  };
+
+  InstallationPrefixDirectory BeginInstallationPrefixDirectory(
     std::string const& programFilesFolderId,
     std::string const& installRootString);
 
-  void EndInstallationPrefixDirectory(size_t size);
+  void EndInstallationPrefixDirectory(
+    InstallationPrefixDirectory installationPrefixDirectory);
 };

+ 8 - 3
Source/CPack/WiX/cmWIXFeaturesSourceWriter.cxx

@@ -5,8 +5,9 @@
 #include "cmStringAlgorithms.h"
 
 cmWIXFeaturesSourceWriter::cmWIXFeaturesSourceWriter(
-  cmCPackLog* logger, std::string const& filename, GuidType componentGuidType)
-  : cmWIXSourceWriter(logger, filename, componentGuidType)
+  unsigned long wixVersion, cmCPackLog* logger, std::string const& filename,
+  GuidType componentGuidType)
+  : cmWIXSourceWriter(wixVersion, logger, filename, componentGuidType)
 {
 }
 
@@ -69,7 +70,11 @@ void cmWIXFeaturesSourceWriter::EmitFeatureForComponent(
   AddAttributeUnlessEmpty("Description", component.Description);
 
   if (component.IsRequired) {
-    AddAttribute("Absent", "disallow");
+    if (this->WixVersion >= 4) {
+      AddAttribute("AllowAbsent", "no");
+    } else {
+      AddAttribute("Absent", "disallow");
+    }
   }
 
   if (component.IsHidden) {

+ 2 - 1
Source/CPack/WiX/cmWIXFeaturesSourceWriter.h

@@ -12,7 +12,8 @@
 class cmWIXFeaturesSourceWriter : public cmWIXSourceWriter
 {
 public:
-  cmWIXFeaturesSourceWriter(cmCPackLog* logger, std::string const& filename,
+  cmWIXFeaturesSourceWriter(unsigned long wixVersion, cmCPackLog* logger,
+                            std::string const& filename,
                             GuidType componentGuidType);
 
   void CreateCMakePackageRegistryEntry(std::string const& package,

+ 3 - 2
Source/CPack/WiX/cmWIXFilesSourceWriter.cxx

@@ -15,10 +15,11 @@
 #include "cmUuid.h"
 #include "cmWIXAccessControlList.h"
 
-cmWIXFilesSourceWriter::cmWIXFilesSourceWriter(cmCPackLog* logger,
+cmWIXFilesSourceWriter::cmWIXFilesSourceWriter(unsigned long wixVersion,
+                                               cmCPackLog* logger,
                                                std::string const& filename,
                                                GuidType componentGuidType)
-  : cmWIXSourceWriter(logger, filename, componentGuidType)
+  : cmWIXSourceWriter(wixVersion, logger, filename, componentGuidType)
 {
 }
 

+ 2 - 1
Source/CPack/WiX/cmWIXFilesSourceWriter.h

@@ -13,7 +13,8 @@
 class cmWIXFilesSourceWriter : public cmWIXSourceWriter
 {
 public:
-  cmWIXFilesSourceWriter(cmCPackLog* logger, std::string const& filename,
+  cmWIXFilesSourceWriter(unsigned long wixVersion, cmCPackLog* logger,
+                         std::string const& filename,
                          GuidType componentGuidType);
 
   void EmitShortcut(std::string const& id, cmWIXShortcut const& shortcut,

+ 27 - 3
Source/CPack/WiX/cmWIXSourceWriter.cxx

@@ -8,11 +8,13 @@
 #include "cmCryptoHash.h"
 #include "cmUuid.h"
 
-cmWIXSourceWriter::cmWIXSourceWriter(cmCPackLog* logger,
+cmWIXSourceWriter::cmWIXSourceWriter(unsigned long wixVersion,
+                                     cmCPackLog* logger,
                                      std::string const& filename,
                                      GuidType componentGuidType,
                                      RootElementType rootElementType)
-  : Logger(logger)
+  : WixVersion(wixVersion)
+  , Logger(logger)
   , File(filename.c_str())
   , State(DEFAULT)
   , SourceFilename(filename)
@@ -26,7 +28,11 @@ cmWIXSourceWriter::cmWIXSourceWriter(cmCPackLog* logger,
     BeginElement("Wix");
   }
 
-  AddAttribute("xmlns", "http://schemas.microsoft.com/wix/2006/wi");
+  if (this->WixVersion >= 4) {
+    AddAttribute("xmlns", "http://wixtoolset.org/schemas/v4/wxs");
+  } else {
+    AddAttribute("xmlns", "http://schemas.microsoft.com/wix/2006/wi");
+  }
 }
 
 cmWIXSourceWriter::~cmWIXSourceWriter()
@@ -42,6 +48,24 @@ cmWIXSourceWriter::~cmWIXSourceWriter()
   EndElement(Elements.back());
 }
 
+void cmWIXSourceWriter::BeginElement_StandardDirectory()
+{
+  if (this->WixVersion >= 4) {
+    BeginElement("StandardDirectory");
+  } else {
+    BeginElement("Directory");
+  }
+}
+
+void cmWIXSourceWriter::EndElement_StandardDirectory()
+{
+  if (this->WixVersion >= 4) {
+    EndElement("StandardDirectory");
+  } else {
+    EndElement("Directory");
+  }
+}
+
 void cmWIXSourceWriter::BeginElement(std::string const& name)
 {
   if (State == BEGIN) {

+ 6 - 2
Source/CPack/WiX/cmWIXSourceWriter.h

@@ -27,12 +27,15 @@ public:
     INCLUDE_ELEMENT_ROOT
   };
 
-  cmWIXSourceWriter(cmCPackLog* logger, std::string const& filename,
-                    GuidType componentGuidType,
+  cmWIXSourceWriter(unsigned long wixVersion, cmCPackLog* logger,
+                    std::string const& filename, GuidType componentGuidType,
                     RootElementType rootElementType = WIX_ELEMENT_ROOT);
 
   ~cmWIXSourceWriter();
 
+  void BeginElement_StandardDirectory();
+  void EndElement_StandardDirectory();
+
   void BeginElement(std::string const& name);
 
   void EndElement(std::string const& name);
@@ -52,6 +55,7 @@ public:
   static std::string EscapeAttributeValue(std::string const& value);
 
 protected:
+  unsigned long WixVersion;
   cmCPackLog* Logger;
 
 private:

+ 2 - 1
Tests/RunCMake/CMakeLists.txt

@@ -1100,9 +1100,10 @@ endif()
 
 add_RunCMake_test_group(CPack "${cpack_tests}")
 
-if(CMake_TEST_CPACK_WIX3)
+if(CMake_TEST_CPACK_WIX3 OR CMake_TEST_CPACK_WIX4)
   add_RunCMake_test(CPack_WIX
     -DCMake_TEST_CPACK_WIX3=${CMake_TEST_CPACK_WIX3}
+    -DCMake_TEST_CPACK_WIX4=${CMake_TEST_CPACK_WIX4}
     )
 endif()
 

+ 1 - 0
Tests/RunCMake/CPack_WIX/4-AppWiX-cpack-WIX-check.cmake

@@ -0,0 +1 @@
+include(${RunCMake_SOURCE_DIR}/cpack-check-common.cmake)

+ 11 - 0
Tests/RunCMake/CPack_WIX/4-AppWiX-cpack-WIX-stdout.txt

@@ -0,0 +1,11 @@
+CPack: Create package using WIX
+CPack: Install projects
+CPack: - Install project: CPackWiXGenerator \[Release\]
+CPack: -   Install component: applications
+CPack: -   Install component: applications2
+CPack: -   Install component: extras
+CPack: -   Install component: headers
+CPack: -   Install component: libraries
+CPack: Create package
+CPack: - package: [^
+]*/Tests/RunCMake/CPack_WIX/4-AppWiX-build/MyLib-1\.0\.0-(win64|windows-arm64)\.msi generated\.

+ 34 - 0
Tests/RunCMake/CPack_WIX/4-AppWiX-verify-stdout.txt

@@ -0,0 +1,34 @@
+-- MyLib-1\.0\.0-(win64|windows-arm64)\.msi
+Component: 'CM_CP_applications.bin.my_libapp.exe' 'CM_DP_applications.bin'
+Component: 'CM_SHORTCUT_applications' 'PROGRAM_MENU_FOLDER'
+Component: 'CM_SHORTCUT_DESKTOP_applications' 'DesktopFolder'
+Component: 'CM_CP_applications2.bin.my_other_app.exe' 'CM_DP_applications2.bin'
+Component: 'CM_SHORTCUT_applications2' 'PROGRAM_MENU_FOLDER'
+Component: 'CM_SHORTCUT_DESKTOP_applications2' 'DesktopFolder'
+Component: 'CM_C_EMPTY_CM_DP_extras.extras.empty' 'CM_DP_extras.extras.empty'
+Component: 'CM_CP_headers.include.file_with_spaces.h' 'CM_DP_headers.include'
+Component: 'CM_CP_headers.include.mylib.h' 'CM_DP_headers.include'
+Component: 'CM_CP_libraries.lib.mylib.lib' 'CM_DP_libraries.lib'
+Directory: 'INSTALL_ROOT' 'ProgramFiles6432Folder' '[^']*\|CPack Component Example'
+Directory: 'CM_DP_applications.bin' 'INSTALL_ROOT' 'bin'
+Directory: 'CM_DP_applications2.bin' 'INSTALL_ROOT' 'bin'
+Directory: 'CM_DP_extras.extras.empty' 'CM_DP_extras.extras' 'empty'
+Directory: 'CM_DP_extras.extras' 'INSTALL_ROOT' 'extras'
+Directory: 'CM_DP_headers.include' 'INSTALL_ROOT' 'include'
+Directory: 'CM_DP_libraries.lib' 'INSTALL_ROOT' 'lib'
+Directory: 'ProgramFiles6432Folder' 'ProgramFiles64Folder' '.'
+Directory: 'PROGRAM_MENU_FOLDER' 'ProgramMenuFolder' 'MyLib'
+Directory: 'ProgramMenuFolder' 'TARGETDIR' 'PMenu'
+Directory: 'ProgramFiles64Folder' 'TARGETDIR' 'PFiles64'
+Directory: 'TARGETDIR' '' 'SourceDir'
+Directory: 'DesktopFolder' 'TARGETDIR' 'Desktop'
+File: 'CM_FP_applications.bin.my_libapp.exe' 'CM_CP_applications.bin.my_libapp.exe' '[^']*\|my-libapp.exe'
+File: 'CM_FP_applications2.bin.my_other_app.exe' 'CM_CP_applications2.bin.my_other_app.exe' '[^']*\|my-other-app.exe'
+File: 'CM_FP_headers.include.file_with_spaces.h' 'CM_CP_headers.include.file_with_spaces.h' '[^']*\|file with spaces.h'
+File: 'CM_FP_headers.include.mylib.h' 'CM_CP_headers.include.mylib.h' 'mylib.h'
+File: 'CM_FP_libraries.lib.mylib.lib' 'CM_CP_libraries.lib.mylib.lib' 'mylib.lib'
+Shortcut: 'CM_SP_applications.bin.my_libapp.exe' 'PROGRAM_MENU_FOLDER' '[^']*\|CPack WiX Test' 'CM_SHORTCUT_applications'
+Shortcut: 'CM_DSP_applications.bin.my_libapp.exe' 'DesktopFolder' '[^']*\|CPack WiX Test' 'CM_SHORTCUT_DESKTOP_applications'
+Shortcut: 'CM_SP_applications2.bin.my_other_app.exe' 'PROGRAM_MENU_FOLDER' '[^']*\|Second CPack WiX Test' 'CM_SHORTCUT_applications2'
+Shortcut: 'CM_DSP_applications2.bin.my_other_app.exe' 'DesktopFolder' '[^']*\|Second CPack WiX Test' 'CM_SHORTCUT_DESKTOP_applications2'
+--

+ 8 - 0
Tests/RunCMake/CPack_WIX/RunCMakeTest.cmake

@@ -7,6 +7,7 @@ set(RunCPack_GLOB *.msi)
 set(RunCPack_VERIFY powershell -ExecutionPolicy Bypass -File ${CMAKE_CURRENT_LIST_DIR}/print-msi.ps1)
 
 function(run_cpack_wix v)
+  set(RunCMake_TEST_OPTIONS -DCPACK_WIX_VERSION=${v})
   run_cpack(${v}-AppWiX SAMPLE AppWiX BUILD)
 endfunction()
 
@@ -14,3 +15,10 @@ if(CMake_TEST_CPACK_WIX3)
   set(ENV{PATH} "${CMake_TEST_CPACK_WIX3};${env_PATH}")
   run_cpack_wix(3)
 endif()
+
+if(CMake_TEST_CPACK_WIX4)
+  set(ENV{PATH} "${CMake_TEST_CPACK_WIX4};${env_PATH}")
+  set(ENV{WIX_EXTENSIONS} "${CMake_TEST_CPACK_WIX4}")
+  run_cpack_wix(4)
+  unset(ENV{WIX_EXTENSIONS})
+endif()