Преглед изворни кода

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 пре 4 недеља
родитељ
комит
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
  In 32-bit builds the token will expand empty while in 64-bit builds
  it will expand to ``64``.
  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
 .. variable:: CPACK_WIX_ROOT
 
 
@@ -451,7 +452,7 @@ Windows using WiX.
  .. versionadded:: 3.29
  .. versionadded:: 3.29
 
 
  This variable can be optionally set to specify the ``InstallScope``
  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``
  ``perMachine``
    Create an installer that installs for all users and requires
    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`.
    This is the default.  See policy :policy:`CMP0172`.
 
 
  ``perUser``
  ``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``
  ``NONE``
    Create an installer without any ``InstallScope`` attribute.
    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
      but the start menu entry and uninstaller registration are created only
      for the current user.
      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
 .. 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 "cmWIXDirectoriesSourceWriter.h"
 #include "cmWIXFeaturesSourceWriter.h"
 #include "cmWIXFeaturesSourceWriter.h"
 #include "cmWIXFilesSourceWriter.h"
 #include "cmWIXFilesSourceWriter.h"
+#include "cmWIXInstallScope.h"
 #include "cmWIXRichTextFormatWriter.h"
 #include "cmWIXRichTextFormatWriter.h"
 #include "cmWIXSourceWriter.h"
 #include "cmWIXSourceWriter.h"
 
 
@@ -545,9 +546,20 @@ bool cmCPackWIXGenerator::CreateWiXSourceFiles()
 
 
   this->WixSources.push_back(directoryDefinitionsFilename);
   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(
   cmWIXDirectoriesSourceWriter directoryDefinitions(
     this->WixVersion, this->Logger, directoryDefinitionsFilename,
     this->WixVersion, this->Logger, directoryDefinitionsFilename,
-    this->ComponentGuidType);
+    this->ComponentGuidType, installScope, componentKeysRegistryPath);
   InjectXmlNamespaces(directoryDefinitions);
   InjectXmlNamespaces(directoryDefinitions);
   directoryDefinitions.BeginElement("Fragment");
   directoryDefinitions.BeginElement("Fragment");
 
 
@@ -565,15 +577,18 @@ bool cmCPackWIXGenerator::CreateWiXSourceFiles()
   auto installationPrefixDirectory =
   auto installationPrefixDirectory =
     directoryDefinitions.BeginInstallationPrefixDirectory(GetRootFolderId(),
     directoryDefinitions.BeginInstallationPrefixDirectory(GetRootFolderId(),
                                                           installRoot);
                                                           installRoot);
+  cm::optional<std::string> removeFolderComponentId =
+    directoryDefinitions.EmitRemoveFolderComponentOnUserInstall(
+      "INSTALL_ROOT");
 
 
   std::string fileDefinitionsFilename =
   std::string fileDefinitionsFilename =
     cmStrCat(this->CPackTopLevel, "/files.wxs");
     cmStrCat(this->CPackTopLevel, "/files.wxs");
 
 
   this->WixSources.push_back(fileDefinitionsFilename);
   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);
   InjectXmlNamespaces(fileDefinitions);
 
 
   fileDefinitions.BeginElement("Fragment");
   fileDefinitions.BeginElement("Fragment");
@@ -622,6 +637,10 @@ bool cmCPackWIXGenerator::CreateWiXSourceFiles()
       *package, GetOption("CPACK_WIX_UPGRADE_GUID"));
       *package, GetOption("CPACK_WIX_UPGRADE_GUID"));
   }
   }
 
 
+  if (removeFolderComponentId.has_value()) {
+    featureDefinitions.EmitComponentRef(*removeFolderComponentId);
+  }
+
   if (!CreateFeatureHierarchy(featureDefinitions)) {
   if (!CreateFeatureHierarchy(featureDefinitions)) {
     return false;
     return false;
   }
   }
@@ -728,6 +747,8 @@ std::string cmCPackWIXGenerator::GetRootFolderId() const
   cmValue rootFolderId = GetOption("CPACK_WIX_ROOT_FOLDER_ID");
   cmValue rootFolderId = GetOption("CPACK_WIX_ROOT_FOLDER_ID");
   if (rootFolderId) {
   if (rootFolderId) {
     result = *rootFolderId;
     result = *rootFolderId;
+  } else if (GetInstallScope() == cmWIXInstallScope::PER_USER) {
+    result = "LocalAppDataFolder";
   } else if (this->WixVersion >= 4) {
   } else if (this->WixVersion >= 4) {
     result = "ProgramFiles6432Folder";
     result = "ProgramFiles6432Folder";
   } else {
   } else {
@@ -1083,6 +1104,13 @@ void cmCPackWIXGenerator::AddDirectoryAndFileDefinitions(
       directoryDefinitions.AddAttribute("Name", fileName);
       directoryDefinitions.AddAttribute("Name", fileName);
       this->Patch->ApplyFragment(subDirectoryId, directoryDefinitions);
       this->Patch->ApplyFragment(subDirectoryId, directoryDefinitions);
 
 
+      cm::optional<std::string> removeFolderComponentId =
+        directoryDefinitions.EmitRemoveFolderComponentOnUserInstall(
+          subDirectoryId);
+      if (removeFolderComponentId.has_value()) {
+        featureDefinitions.EmitComponentRef(*removeFolderComponentId);
+      }
+
       AddDirectoryAndFileDefinitions(fullPath, subDirectoryId,
       AddDirectoryAndFileDefinitions(fullPath, subDirectoryId,
                                      directoryDefinitions, fileDefinitions,
                                      directoryDefinitions, fileDefinitions,
                                      featureDefinitions, packageExecutables,
                                      featureDefinitions, packageExecutables,
@@ -1374,3 +1402,24 @@ void cmCPackWIXGenerator::InjectXmlNamespaces(cmWIXSourceWriter& sourceWriter)
                                          ns.second);
                                          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 <string>
 
 
 #include "cmCPackGenerator.h"
 #include "cmCPackGenerator.h"
+#include "cmWIXInstallScope.h"
 #include "cmWIXPatch.h"
 #include "cmWIXPatch.h"
 #include "cmWIXShortcut.h"
 #include "cmWIXShortcut.h"
 
 
@@ -159,6 +160,8 @@ private:
 
 
   void InjectXmlNamespaces(cmWIXSourceWriter& sourceWriter);
   void InjectXmlNamespaces(cmWIXSourceWriter& sourceWriter);
 
 
+  cmWIXInstallScope GetInstallScope() const;
+
   std::vector<std::string> WixSources;
   std::vector<std::string> WixSources;
   id_map_t PathToIdMap;
   id_map_t PathToIdMap;
   ambiguity_map_t IdAmbiguityCounter;
   ambiguity_map_t IdAmbiguityCounter;

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

@@ -6,9 +6,24 @@
 
 
 cmWIXDirectoriesSourceWriter::cmWIXDirectoriesSourceWriter(
 cmWIXDirectoriesSourceWriter::cmWIXDirectoriesSourceWriter(
   unsigned long wixVersion, cmCPackLog* logger, std::string const& filename,
   unsigned long wixVersion, cmCPackLog* logger, std::string const& filename,
-  GuidType componentGuidType)
+  GuidType componentGuidType, cmWIXInstallScope installScope,
+  std::string componentKeysRegistryPath)
   : cmWIXSourceWriter(wixVersion, logger, filename, componentGuidType)
   : 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(
 void cmWIXDirectoriesSourceWriter::EmitStartMenuFolder(
@@ -47,6 +62,43 @@ void cmWIXDirectoriesSourceWriter::EmitStartupFolder()
   EndElement_StandardDirectory();
   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::InstallationPrefixDirectory
 cmWIXDirectoriesSourceWriter::BeginInstallationPrefixDirectory(
 cmWIXDirectoriesSourceWriter::BeginInstallationPrefixDirectory(
   std::string const& programFilesFolderId,
   std::string const& programFilesFolderId,

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

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

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

@@ -19,12 +19,26 @@
 #  include "cmsys/Encoding.hxx"
 #  include "cmsys/Encoding.hxx"
 #endif
 #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)
   : 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,
 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 componentId = std::string("CM_C") + id;
   std::string fileId = std::string("CM_F") + 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");
   BeginElement("DirectoryRef");
   AddAttribute("Id", directoryId);
   AddAttribute("Id", directoryId);
@@ -150,6 +168,22 @@ std::string cmWIXFilesSourceWriter::EmitComponentFile(
   }
   }
 
 
   patch.ApplyFragment(componentId, *this);
   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");
   BeginElement("File");
   AddAttribute("Id", fileId);
   AddAttribute("Id", fileId);
 
 
@@ -164,7 +198,9 @@ std::string cmWIXFilesSourceWriter::EmitComponentFile(
 #endif
 #endif
   AddAttribute("Source", sourcePath);
   AddAttribute("Source", sourcePath);
 
 
-  AddAttribute("KeyPath", "yes");
+  if (!this->PerUserInstall) {
+    AddAttribute("KeyPath", "yes");
+  }
 
 
   mode_t fileMode = 0;
   mode_t fileMode = 0;
   cmSystemTools::GetPermissions(filePath.c_str(), fileMode);
   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.  */
    file LICENSE.rst or https://cmake.org/licensing for details.  */
 #pragma once
 #pragma once
 
 
+#include <string>
+
 #include "cmCPackGenerator.h"
 #include "cmCPackGenerator.h"
+#include "cmWIXInstallScope.h"
 #include "cmWIXPatch.h"
 #include "cmWIXPatch.h"
 #include "cmWIXShortcut.h"
 #include "cmWIXShortcut.h"
 #include "cmWIXSourceWriter.h"
 #include "cmWIXSourceWriter.h"
@@ -15,7 +18,9 @@ class cmWIXFilesSourceWriter : public cmWIXSourceWriter
 public:
 public:
   cmWIXFilesSourceWriter(unsigned long wixVersion, cmCPackLog* logger,
   cmWIXFilesSourceWriter(unsigned long wixVersion, cmCPackLog* logger,
                          std::string const& filename,
                          std::string const& filename,
-                         GuidType componentGuidType);
+                         GuidType componentGuidType,
+                         cmWIXInstallScope installScope,
+                         std::string componentKeysRegistryPath);
 
 
   void EmitShortcut(std::string const& id, cmWIXShortcut const& shortcut,
   void EmitShortcut(std::string const& id, cmWIXShortcut const& shortcut,
                     std::string const& shortcutPrefix, size_t shortcutIndex);
                     std::string const& shortcutPrefix, size_t shortcutIndex);
@@ -37,4 +42,8 @@ public:
                                 std::string const& filePath, cmWIXPatch& patch,
                                 std::string const& filePath, cmWIXPatch& patch,
                                 cmInstalledFile const* installedFile,
                                 cmInstalledFile const* installedFile,
                                 int diskId);
                                 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 = "*";
   std::string guid = "*";
   if (this->ComponentGuidType == CMAKE_GENERATED_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;
   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()
 void cmWIXSourceWriter::WriteXMLDeclaration()
 {
 {
   File << R"(<?xml version="1.0" encoding="UTF-8"?>)" << std::endl;
   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 <string>
 #include <vector>
 #include <vector>
 
 
+#include <cm/optional>
+
 #include "cmsys/FStream.hxx"
 #include "cmsys/FStream.hxx"
 
 
 #include "cmCPackLog.h"
 #include "cmCPackLog.h"
@@ -52,6 +54,12 @@ public:
 
 
   std::string CreateGuidFromComponentId(std::string const& componentId);
   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);
   static std::string EscapeAttributeValue(std::string const& value);
 
 
 protected:
 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)
   run_cpack(${v}-AppWiX SAMPLE AppWiX BUILD)
 endfunction()
 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)
 if(CMake_TEST_CPACK_WIX3)
   set(ENV{PATH} "${CMake_TEST_CPACK_WIX3};${env_PATH}")
   set(ENV{PATH} "${CMake_TEST_CPACK_WIX3};${env_PATH}")
   run_cpack_wix(3)
   run_cpack_wix(3)
+  run_cpack_wix_peruser(3)
 endif()
 endif()
 
 
 if(CMake_TEST_CPACK_WIX4)
 if(CMake_TEST_CPACK_WIX4)
   set(ENV{PATH} "${CMake_TEST_CPACK_WIX4};${env_PATH}")
   set(ENV{PATH} "${CMake_TEST_CPACK_WIX4};${env_PATH}")
   set(ENV{WIX_EXTENSIONS} "${CMake_TEST_CPACK_WIX4}")
   set(ENV{WIX_EXTENSIONS} "${CMake_TEST_CPACK_WIX4}")
   run_cpack_wix(4)
   run_cpack_wix(4)
+  run_cpack_wix_peruser(4)
   unset(ENV{WIX_EXTENSIONS})
   unset(ENV{WIX_EXTENSIONS})
 endif()
 endif()