Browse Source

Merge topic 'cpack-wix-perUser'

781754f505 CPack/WiX: Add support for perUser install scope

Acked-by: Kitware Robot <[email protected]>
Merge-request: !11680
Brad King 2 weeks ago
parent
commit
a4373ca252

+ 40 - 12
Help/cpack_gen/wix.rst

@@ -408,8 +408,9 @@ Windows using WiX.
  In 32-bit builds the token will expand empty while in 64-bit builds
  it will expand to ``64``.
 
- When unset generated installers will default installing to
- ``ProgramFiles<64>Folder``.
+ When unset generated installers will install to
+ ``LocalAppDataFolder`` if :variable:`CPACK_WIX_INSTALL_SCOPE` is ``perUser``,
+ and to ``ProgramFiles<64>Folder`` otherwise.
 
 .. variable:: CPACK_WIX_ROOT
 
@@ -451,7 +452,7 @@ Windows using WiX.
  .. versionadded:: 3.29
 
  This variable can be optionally set to specify the ``InstallScope``
- of the installer:
+ of the installer (see https://docs.firegiant.com/wix3/xsd/wix/package/):
 
  ``perMachine``
    Create an installer that installs for all users and requires
@@ -461,7 +462,20 @@ Windows using WiX.
    This is the default.  See policy :policy:`CMP0172`.
 
  ``perUser``
-   Not yet supported. This is reserved for future use.
+   Create an installer that installs only for the current user
+   and does not require administrative privileges. Start menu entries created
+   by the installer are visible only to the current user.
+
+   To enable per-user installation, the installer has to generate some
+   additional registry entries to serve as "key paths" for installed
+   components (see https://learn.microsoft.com/en-us/windows/win32/msi/ice38).
+   These registry entries are created under ``HKEY_CURRENT_USER``, using the
+   path specified by the :variable:`CPACK_WIX_COMPONENT_KEYS_REGISTRY_PATH`
+   variable.
+
+   .. versionchanged:: 4.3
+
+     Before CMake 4.3, this value was reserved for future use and not supported.
 
  ``NONE``
    Create an installer without any ``InstallScope`` attribute.
@@ -477,16 +491,30 @@ Windows using WiX.
      but the start menu entry and uninstaller registration are created only
      for the current user.
 
-   .. warning::
+ .. warning::
+   Installations performed by installers created with different
+   ``InstallScope`` values cannot be cleanly updated or replaced by each other.
+   For example, to transition a project's installers from ``NONE`` to
+   ``perMachine``, or from ``perMachine`` to ``perUser``, the latter installer
+   should be distributed with instructions to first manually uninstall
+   any older version.
+
+.. variable:: CPACK_WIX_COMPONENT_KEYS_REGISTRY_PATH
+
+ .. versionadded:: 4.3
+
+ This variable determines the registry path under ``HKEY_CURRENT_USER``
+ where the installer will create registry entries to serve as "key paths"
+ for components if :variable:`CPACK_WIX_INSTALL_SCOPE` is set to ``perUser``.
+
+ Use forward slashes (``/``) as path separators to avoid issues with escapes;
+ they will be converted to backslashes (``\``) by the WIX generator.
 
-     An installation performed by an installer created without any
-     ``InstallScope`` cannot be cleanly updated or replaced by an
-     installer with an ``InstallScope``.  In order to transition
-     a project's installers from ``NONE`` to ``perMachine``, the
-     latter installer should be distributed with instructions to
-     first manually uninstall any older version.
+ Default value is ``Software/<Vendor>/<PackageName>/Components``,
+ where ``<Vendor>`` is taken from :variable:`CPACK_PACKAGE_VENDOR`,
+ and ``<PackageName>`` is taken from :variable:`CPACK_PACKAGE_NAME`.
 
- See https://docs.firegiant.com/wix3/xsd/wix/package/
+ Example: ``Software/MyCompany/MyProduct/Components``
 
 .. variable:: CPACK_WIX_CAB_PER_COMPONENT
 

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

@@ -0,0 +1,5 @@
+cpack-wix-perUser
+-----------------
+
+* The :cpack_gen:`CPack WIX Generator` now supports per-user installers
+  by setting :variable:`CPACK_WIX_INSTALL_SCOPE` to ``perUser``.

+ 53 - 4
Source/CPack/WiX/cmCPackWIXGenerator.cxx

@@ -27,6 +27,7 @@
 #include "cmWIXDirectoriesSourceWriter.h"
 #include "cmWIXFeaturesSourceWriter.h"
 #include "cmWIXFilesSourceWriter.h"
+#include "cmWIXInstallScope.h"
 #include "cmWIXRichTextFormatWriter.h"
 #include "cmWIXSourceWriter.h"
 
@@ -545,9 +546,20 @@ bool cmCPackWIXGenerator::CreateWiXSourceFiles()
 
   this->WixSources.push_back(directoryDefinitionsFilename);
 
+  cmWIXInstallScope installScope = GetInstallScope();
+
+  std::string componentKeysRegistryPath =
+    GetOption("CPACK_WIX_COMPONENT_KEYS_REGISTRY_PATH");
+  if (componentKeysRegistryPath.empty()) {
+    componentKeysRegistryPath =
+      cmStrCat("Software\\", GetOption("CPACK_PACKAGE_VENDOR"), "\\",
+               GetOption("CPACK_PACKAGE_NAME"), "\\Components");
+  }
+  cmSystemTools::ReplaceString(componentKeysRegistryPath, "/", "\\");
+
   cmWIXDirectoriesSourceWriter directoryDefinitions(
     this->WixVersion, this->Logger, directoryDefinitionsFilename,
-    this->ComponentGuidType);
+    this->ComponentGuidType, installScope, componentKeysRegistryPath);
   InjectXmlNamespaces(directoryDefinitions);
   directoryDefinitions.BeginElement("Fragment");
 
@@ -565,15 +577,18 @@ bool cmCPackWIXGenerator::CreateWiXSourceFiles()
   auto installationPrefixDirectory =
     directoryDefinitions.BeginInstallationPrefixDirectory(GetRootFolderId(),
                                                           installRoot);
+  cm::optional<std::string> removeFolderComponentId =
+    directoryDefinitions.EmitRemoveFolderComponentOnUserInstall(
+      "INSTALL_ROOT");
 
   std::string fileDefinitionsFilename =
     cmStrCat(this->CPackTopLevel, "/files.wxs");
 
   this->WixSources.push_back(fileDefinitionsFilename);
 
-  cmWIXFilesSourceWriter fileDefinitions(this->WixVersion, this->Logger,
-                                         fileDefinitionsFilename,
-                                         this->ComponentGuidType);
+  cmWIXFilesSourceWriter fileDefinitions(
+    this->WixVersion, this->Logger, fileDefinitionsFilename,
+    this->ComponentGuidType, installScope, componentKeysRegistryPath);
   InjectXmlNamespaces(fileDefinitions);
 
   fileDefinitions.BeginElement("Fragment");
@@ -622,6 +637,10 @@ bool cmCPackWIXGenerator::CreateWiXSourceFiles()
       *package, GetOption("CPACK_WIX_UPGRADE_GUID"));
   }
 
+  if (removeFolderComponentId.has_value()) {
+    featureDefinitions.EmitComponentRef(*removeFolderComponentId);
+  }
+
   if (!CreateFeatureHierarchy(featureDefinitions)) {
     return false;
   }
@@ -728,6 +747,8 @@ std::string cmCPackWIXGenerator::GetRootFolderId() const
   cmValue rootFolderId = GetOption("CPACK_WIX_ROOT_FOLDER_ID");
   if (rootFolderId) {
     result = *rootFolderId;
+  } else if (GetInstallScope() == cmWIXInstallScope::PER_USER) {
+    result = "LocalAppDataFolder";
   } else if (this->WixVersion >= 4) {
     result = "ProgramFiles6432Folder";
   } else {
@@ -1083,6 +1104,13 @@ void cmCPackWIXGenerator::AddDirectoryAndFileDefinitions(
       directoryDefinitions.AddAttribute("Name", fileName);
       this->Patch->ApplyFragment(subDirectoryId, directoryDefinitions);
 
+      cm::optional<std::string> removeFolderComponentId =
+        directoryDefinitions.EmitRemoveFolderComponentOnUserInstall(
+          subDirectoryId);
+      if (removeFolderComponentId.has_value()) {
+        featureDefinitions.EmitComponentRef(*removeFolderComponentId);
+      }
+
       AddDirectoryAndFileDefinitions(fullPath, subDirectoryId,
                                      directoryDefinitions, fileDefinitions,
                                      featureDefinitions, packageExecutables,
@@ -1374,3 +1402,24 @@ void cmCPackWIXGenerator::InjectXmlNamespaces(cmWIXSourceWriter& sourceWriter)
                                          ns.second);
   }
 }
+
+cmWIXInstallScope cmCPackWIXGenerator::GetInstallScope() const
+{
+  cmValue value = this->GetOption("CPACK_WIX_INSTALL_SCOPE");
+
+  if (value == "perUser"_s) {
+    return cmWIXInstallScope::PER_USER;
+  }
+  if (value == "perMachine"_s) {
+    return cmWIXInstallScope::PER_MACHINE;
+  }
+  if (value == "NONE"_s) {
+    return cmWIXInstallScope::NONE;
+  }
+
+  cmCPackLogger(
+    cmCPackLog::LOG_ERROR,
+    "Invalid CPACK_WIX_INSTALL_SCOPE value, defaulting to perMachine: "
+      << (value ? *value : "") << std::endl);
+  return cmWIXInstallScope::PER_MACHINE;
+}

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

@@ -7,6 +7,7 @@
 #include <string>
 
 #include "cmCPackGenerator.h"
+#include "cmWIXInstallScope.h"
 #include "cmWIXPatch.h"
 #include "cmWIXShortcut.h"
 
@@ -159,6 +160,8 @@ private:
 
   void InjectXmlNamespaces(cmWIXSourceWriter& sourceWriter);
 
+  cmWIXInstallScope GetInstallScope() const;
+
   std::vector<std::string> WixSources;
   id_map_t PathToIdMap;
   ambiguity_map_t IdAmbiguityCounter;

+ 53 - 1
Source/CPack/WiX/cmWIXDirectoriesSourceWriter.cxx

@@ -6,9 +6,24 @@
 
 cmWIXDirectoriesSourceWriter::cmWIXDirectoriesSourceWriter(
   unsigned long wixVersion, cmCPackLog* logger, std::string const& filename,
-  GuidType componentGuidType)
+  GuidType componentGuidType, cmWIXInstallScope installScope,
+  std::string componentKeysRegistryPath)
   : cmWIXSourceWriter(wixVersion, logger, filename, componentGuidType)
+  , ComponentKeysRegistryPath(std::move(componentKeysRegistryPath))
 {
+  switch (installScope) {
+    case cmWIXInstallScope::PER_USER:
+      this->PerUserInstall = true;
+      break;
+    case cmWIXInstallScope::PER_MACHINE:
+    case cmWIXInstallScope::NONE:
+      this->PerUserInstall = false;
+      break;
+    default:
+      cmCPackLogger(cmCPackLog::LOG_ERROR,
+                    "Unhandled install scope value, this is a CPack Bug.");
+      break;
+  }
 }
 
 void cmWIXDirectoriesSourceWriter::EmitStartMenuFolder(
@@ -47,6 +62,43 @@ void cmWIXDirectoriesSourceWriter::EmitStartupFolder()
   EndElement_StandardDirectory();
 }
 
+cm::optional<std::string>
+cmWIXDirectoriesSourceWriter::EmitRemoveFolderComponentOnUserInstall(
+  std::string const& directoryId)
+{
+  if (!this->PerUserInstall) {
+    return {};
+  }
+
+  BeginElement("Component");
+
+  std::string componentId = directoryId + "_RemoveFolderComponent";
+  AddAttribute("Id", componentId);
+  AddAttribute("Guid", CreateGuidFromComponentId(componentId));
+
+  BeginElement("RemoveFolder");
+  AddAttribute("Id", directoryId + "_RemoveFolder");
+  AddAttribute("Directory", directoryId);
+  AddAttribute("On", "uninstall");
+  EndElement("RemoveFolder");
+
+  // Need a keyPath for the component
+  BeginElement("RegistryValue");
+
+  AddAttribute("Root", "HKCU");
+  AddAttribute("Key", this->ComponentKeysRegistryPath);
+  AddAttribute("Name", componentId);
+  AddAttribute("Type", "string");
+  AddAttribute("Value", "1");
+  AddAttribute("KeyPath", "yes");
+
+  EndElement("RegistryValue");
+
+  EndElement("Component");
+
+  return componentId;
+}
+
 cmWIXDirectoriesSourceWriter::InstallationPrefixDirectory
 cmWIXDirectoriesSourceWriter::BeginInstallationPrefixDirectory(
   std::string const& programFilesFolderId,

+ 13 - 1
Source/CPack/WiX/cmWIXDirectoriesSourceWriter.h

@@ -4,7 +4,10 @@
 
 #include <string>
 
+#include <cm/optional>
+
 #include "cmCPackGenerator.h"
+#include "cmWIXInstallScope.h"
 #include "cmWIXSourceWriter.h"
 
 /** \class cmWIXDirectoriesSourceWriter
@@ -15,7 +18,9 @@ class cmWIXDirectoriesSourceWriter : public cmWIXSourceWriter
 public:
   cmWIXDirectoriesSourceWriter(unsigned long wixVersion, cmCPackLog* logger,
                                std::string const& filename,
-                               GuidType componentGuidType);
+                               GuidType componentGuidType,
+                               cmWIXInstallScope installScope,
+                               std::string componentKeysRegistryPath);
 
   void EmitStartMenuFolder(std::string const& startMenuFolder);
 
@@ -23,6 +28,9 @@ public:
 
   void EmitStartupFolder();
 
+  cm::optional<std::string> EmitRemoveFolderComponentOnUserInstall(
+    std::string const& directoryId);
+
   struct InstallationPrefixDirectory
   {
     bool HasStandardDirectory = false;
@@ -35,4 +43,8 @@ public:
 
   void EndInstallationPrefixDirectory(
     InstallationPrefixDirectory installationPrefixDirectory);
+
+private:
+  bool PerUserInstall = false;
+  std::string ComponentKeysRegistryPath;
 };

+ 42 - 6
Source/CPack/WiX/cmWIXFilesSourceWriter.cxx

@@ -19,12 +19,26 @@
 #  include "cmsys/Encoding.hxx"
 #endif
 
-cmWIXFilesSourceWriter::cmWIXFilesSourceWriter(unsigned long wixVersion,
-                                               cmCPackLog* logger,
-                                               std::string const& filename,
-                                               GuidType componentGuidType)
+cmWIXFilesSourceWriter::cmWIXFilesSourceWriter(
+  unsigned long wixVersion, cmCPackLog* logger, std::string const& filename,
+  GuidType componentGuidType, cmWIXInstallScope installScope,
+  std::string componentKeysRegistryPath)
   : cmWIXSourceWriter(wixVersion, logger, filename, componentGuidType)
+  , ComponentKeysRegistryPath(std::move(componentKeysRegistryPath))
 {
+  switch (installScope) {
+    case cmWIXInstallScope::PER_USER:
+      this->PerUserInstall = true;
+      break;
+    case cmWIXInstallScope::PER_MACHINE:
+    case cmWIXInstallScope::NONE:
+      this->PerUserInstall = false;
+      break;
+    default:
+      cmCPackLogger(cmCPackLog::LOG_ERROR,
+                    "Unhandled install scope value, this is a CPack Bug.");
+      break;
+  }
 }
 
 void cmWIXFilesSourceWriter::EmitShortcut(std::string const& id,
@@ -127,7 +141,11 @@ std::string cmWIXFilesSourceWriter::EmitComponentFile(
   std::string componentId = std::string("CM_C") + id;
   std::string fileId = std::string("CM_F") + id;
 
-  std::string guid = CreateGuidFromComponentId(componentId);
+  // Wix doesn't support automatic GUIDs for components which have both
+  // registry entries and files.
+  std::string guid = this->PerUserInstall
+    ? CreateCmakeGeneratedGuidFromComponentId(componentId)
+    : CreateGuidFromComponentId(componentId);
 
   BeginElement("DirectoryRef");
   AddAttribute("Id", directoryId);
@@ -150,6 +168,22 @@ std::string cmWIXFilesSourceWriter::EmitComponentFile(
   }
 
   patch.ApplyFragment(componentId, *this);
+
+  if (this->PerUserInstall) {
+    // For perUser installs, MSI requires using a registry entry as the key
+    // path for components.
+    BeginElement("RegistryValue");
+
+    AddAttribute("Root", "HKCU");
+    AddAttribute("Key", this->ComponentKeysRegistryPath);
+    AddAttribute("Name", fileId);
+    AddAttribute("Type", "string");
+    AddAttribute("Value", "1");
+    AddAttribute("KeyPath", "yes");
+
+    EndElement("RegistryValue");
+  }
+
   BeginElement("File");
   AddAttribute("Id", fileId);
 
@@ -164,7 +198,9 @@ std::string cmWIXFilesSourceWriter::EmitComponentFile(
 #endif
   AddAttribute("Source", sourcePath);
 
-  AddAttribute("KeyPath", "yes");
+  if (!this->PerUserInstall) {
+    AddAttribute("KeyPath", "yes");
+  }
 
   mode_t fileMode = 0;
   cmSystemTools::GetPermissions(filePath.c_str(), fileMode);

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

@@ -2,7 +2,10 @@
    file LICENSE.rst or https://cmake.org/licensing for details.  */
 #pragma once
 
+#include <string>
+
 #include "cmCPackGenerator.h"
+#include "cmWIXInstallScope.h"
 #include "cmWIXPatch.h"
 #include "cmWIXShortcut.h"
 #include "cmWIXSourceWriter.h"
@@ -15,7 +18,9 @@ class cmWIXFilesSourceWriter : public cmWIXSourceWriter
 public:
   cmWIXFilesSourceWriter(unsigned long wixVersion, cmCPackLog* logger,
                          std::string const& filename,
-                         GuidType componentGuidType);
+                         GuidType componentGuidType,
+                         cmWIXInstallScope installScope,
+                         std::string componentKeysRegistryPath);
 
   void EmitShortcut(std::string const& id, cmWIXShortcut const& shortcut,
                     std::string const& shortcutPrefix, size_t shortcutIndex);
@@ -37,4 +42,8 @@ public:
                                 std::string const& filePath, cmWIXPatch& patch,
                                 cmInstalledFile const* installedFile,
                                 int diskId);
+
+private:
+  bool PerUserInstall = false;
+  std::string ComponentKeysRegistryPath;
 };

+ 10 - 0
Source/CPack/WiX/cmWIXInstallScope.h

@@ -0,0 +1,10 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file LICENSE.rst or https://cmake.org/licensing for details.  */
+#pragma once
+
+enum class cmWIXInstallScope
+{
+  NONE,
+  PER_MACHINE,
+  PER_USER
+};

+ 11 - 5
Source/CPack/WiX/cmWIXSourceWriter.cxx

@@ -159,15 +159,21 @@ std::string cmWIXSourceWriter::CreateGuidFromComponentId(
 {
   std::string guid = "*";
   if (this->ComponentGuidType == CMAKE_GENERATED_GUID) {
-    cmCryptoHash hasher(cmCryptoHash::AlgoMD5);
-    std::string md5 = hasher.HashString(componentId);
-    cmUuid uuid;
-    std::vector<unsigned char> ns;
-    guid = uuid.FromMd5(ns, md5);
+    guid = CreateCmakeGeneratedGuidFromComponentId(componentId);
   }
   return guid;
 }
 
+std::string cmWIXSourceWriter::CreateCmakeGeneratedGuidFromComponentId(
+  std::string const& componentId)
+{
+  cmCryptoHash hasher(cmCryptoHash::AlgoMD5);
+  std::string md5 = hasher.HashString(componentId);
+  cmUuid uuid;
+  std::vector<unsigned char> ns;
+  return uuid.FromMd5(ns, md5);
+}
+
 void cmWIXSourceWriter::WriteXMLDeclaration()
 {
   File << R"(<?xml version="1.0" encoding="UTF-8"?>)" << std::endl;

+ 8 - 0
Source/CPack/WiX/cmWIXSourceWriter.h

@@ -5,6 +5,8 @@
 #include <string>
 #include <vector>
 
+#include <cm/optional>
+
 #include "cmsys/FStream.hxx"
 
 #include "cmCPackLog.h"
@@ -52,6 +54,12 @@ public:
 
   std::string CreateGuidFromComponentId(std::string const& componentId);
 
+  // In most cases CreateGuidFromComponentId should be used instead, since it
+  // takes ComponentGuidType into consideration. This function always generates
+  // an explicit GUID.
+  static std::string CreateCmakeGeneratedGuidFromComponentId(
+    std::string const& componentId);
+
   static std::string EscapeAttributeValue(std::string const& value);
 
 protected:

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

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

+ 40 - 0
Tests/RunCMake/CPack_WIX/3-AppWiX-perUser-verify-stdout.txt

@@ -0,0 +1,40 @@
+-- MyLib-1\.0\.0-(win64|windows-arm64)\.msi
+Component: 'INSTALL_ROOT_RemoveFolderComponent' 'INSTALL_ROOT'
+Component: 'CM_DP_applications\.bin_RemoveFolderComponent' 'CM_DP_applications\.bin'
+Component: 'CM_DP_applications2\.bin_RemoveFolderComponent' 'CM_DP_applications2\.bin'
+Component: 'CM_DP_extras\.extras_RemoveFolderComponent' 'CM_DP_extras\.extras'
+Component: 'CM_DP_extras\.extras\.empty_RemoveFolderComponent' 'CM_DP_extras\.extras\.empty'
+Component: 'CM_DP_headers\.include_RemoveFolderComponent' 'CM_DP_headers\.include'
+Component: 'CM_DP_libraries\.lib_RemoveFolderComponent' 'CM_DP_libraries\.lib'
+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' 'LocalAppDataFolder' '[^']*\|CPack Component Example'
+Directory: 'CM_DP_applications\.bin' 'INSTALL_ROOT' 'bin'
+Directory: 'CM_DP_applications2\.bin' 'INSTALL_ROOT' 'bin'
+Directory: 'CM_DP_extras\.extras' 'INSTALL_ROOT' 'extras'
+Directory: 'CM_DP_extras\.extras\.empty' 'CM_DP_extras\.extras' 'empty'
+Directory: 'CM_DP_headers\.include' 'INSTALL_ROOT' 'include'
+Directory: 'CM_DP_libraries\.lib' 'INSTALL_ROOT' 'lib'
+Directory: 'PROGRAM_MENU_FOLDER' 'ProgramMenuFolder' 'MyLib'
+Directory: 'DesktopFolder' 'TARGETDIR' 'Desktop'
+Directory: 'LocalAppDataFolder' 'TARGETDIR' '\.'
+Directory: 'TARGETDIR' '' 'SourceDir'
+Directory: 'ProgramMenuFolder' 'TARGETDIR' '\.'
+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'
+--

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

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

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

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

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

@@ -0,0 +1,40 @@
+-- MyLib-1\.0\.0-(win64|windows-arm64)\.msi
+Component: 'INSTALL_ROOT_RemoveFolderComponent' 'INSTALL_ROOT'
+Component: 'CM_DP_applications\.bin_RemoveFolderComponent' 'CM_DP_applications\.bin'
+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_DP_applications2\.bin_RemoveFolderComponent' 'CM_DP_applications2\.bin'
+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_DP_extras\.extras_RemoveFolderComponent' 'CM_DP_extras\.extras'
+Component: 'CM_DP_extras\.extras\.empty_RemoveFolderComponent' 'CM_DP_extras\.extras\.empty'
+Component: 'CM_C_EMPTY_CM_DP_extras\.extras\.empty' 'CM_DP_extras\.extras\.empty'
+Component: 'CM_DP_headers\.include_RemoveFolderComponent' 'CM_DP_headers\.include'
+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_DP_libraries\.lib_RemoveFolderComponent' 'CM_DP_libraries\.lib'
+Component: 'CM_CP_libraries\.lib\.mylib\.lib' 'CM_DP_libraries\.lib'
+Directory: 'INSTALL_ROOT' 'LocalAppDataFolder' '[^']*\|CPack Component Example'
+Directory: 'CM_DP_applications\.bin' 'INSTALL_ROOT' 'bin'
+Directory: 'CM_DP_applications2\.bin' 'INSTALL_ROOT' 'bin'
+Directory: 'CM_DP_extras\.extras' 'INSTALL_ROOT' 'extras'
+Directory: 'CM_DP_extras\.extras\.empty' 'CM_DP_extras\.extras' 'empty'
+Directory: 'CM_DP_headers\.include' 'INSTALL_ROOT' 'include'
+Directory: 'CM_DP_libraries\.lib' 'INSTALL_ROOT' 'lib'
+Directory: 'PROGRAM_MENU_FOLDER' 'ProgramMenuFolder' 'MyLib'
+Directory: 'DesktopFolder' 'TARGETDIR' 'Desktop'
+Directory: 'LocalAppDataFolder' 'TARGETDIR' 'LocalApp'
+Directory: 'ProgramMenuFolder' 'TARGETDIR' 'PMenu'
+Directory: 'TARGETDIR' '' 'SourceDir'
+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'
+--

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

@@ -11,14 +11,21 @@ function(run_cpack_wix v)
   run_cpack(${v}-AppWiX SAMPLE AppWiX BUILD)
 endfunction()
 
+function(run_cpack_wix_peruser v)
+  set(RunCMake_TEST_OPTIONS -DCPACK_WIX_VERSION=${v} -DCPACK_WIX_INSTALL_SCOPE=perUser)
+  run_cpack(${v}-AppWiX-perUser SAMPLE AppWiX BUILD)
+endfunction()
+
 if(CMake_TEST_CPACK_WIX3)
   set(ENV{PATH} "${CMake_TEST_CPACK_WIX3};${env_PATH}")
   run_cpack_wix(3)
+  run_cpack_wix_peruser(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)
+  run_cpack_wix_peruser(4)
   unset(ENV{WIX_EXTENSIONS})
 endif()