Merge topic 'xcode-framework-system-includes'

7d99cff236 Xcode: Properly identify frameworks with system includes

Acked-by: Kitware Robot <kwrobot@kitware.com>
Tested-by: buildbot <buildbot@kitware.com>
Merge-request: !6815
diff --git a/.gitlab/ci/configure_windows_vs_common.cmake b/.gitlab/ci/configure_windows_vs_common.cmake
index a0a4742..703f534 100644
--- a/.gitlab/ci/configure_windows_vs_common.cmake
+++ b/.gitlab/ci/configure_windows_vs_common.cmake
@@ -4,5 +4,6 @@
 set(CMake_TEST_FindOpenMP_C "ON" CACHE BOOL "")
 set(CMake_TEST_FindOpenMP_CXX "ON" CACHE BOOL "")
 set(CMake_TEST_FindOpenMP_Fortran "OFF" CACHE BOOL "")
+set(CMake_TEST_Java OFF CACHE BOOL "")
 
 include("${CMAKE_CURRENT_LIST_DIR}/configure_external_test.cmake")
diff --git a/Auxiliary/vim/syntax/cmake.vim b/Auxiliary/vim/syntax/cmake.vim
index 52330f7..c6c5d23 100644
--- a/Auxiliary/vim/syntax/cmake.vim
+++ b/Auxiliary/vim/syntax/cmake.vim
@@ -152,6 +152,7 @@
             \ DISABLED
             \ DISABLED_FEATURES
             \ DISABLE_PRECOMPILE_HEADERS
+            \ DOTNET_SDK
             \ DOTNET_TARGET_FRAMEWORK
             \ DOTNET_TARGET_FRAMEWORK_VERSION
             \ ECLIPSE_EXTRA_CPROJECT_CONTENTS
@@ -1001,6 +1002,7 @@
             \ CMAKE_DIRECTORY_LABELS
             \ CMAKE_DISABLE_PRECOMPILE_HEADERS
             \ CMAKE_DL_LIBS
+            \ CMAKE_DOTNET_SDK
             \ CMAKE_DOTNET_TARGET_FRAMEWORK
             \ CMAKE_DOTNET_TARGET_FRAMEWORK_VERSION
             \ CMAKE_ECLIPSE_GENERATE_LINKED_RESOURCES
diff --git a/Copyright.txt b/Copyright.txt
index b703193..ed25419 100644
--- a/Copyright.txt
+++ b/Copyright.txt
@@ -1,5 +1,5 @@
 CMake - Cross Platform Makefile Generator
-Copyright 2000-2021 Kitware, Inc. and Contributors
+Copyright 2000-2022 Kitware, Inc. and Contributors
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
diff --git a/Help/command/if.rst b/Help/command/if.rst
index 6957142..a729b1e 100644
--- a/Help/command/if.rst
+++ b/Help/command/if.rst
@@ -52,7 +52,8 @@
 
 ``if(<constant>)``
  True if the constant is ``1``, ``ON``, ``YES``, ``TRUE``, ``Y``,
- or a non-zero number.  False if the constant is ``0``, ``OFF``,
+ or a non-zero number (including floating point numbers).
+ False if the constant is ``0``, ``OFF``,
  ``NO``, ``FALSE``, ``N``, ``IGNORE``, ``NOTFOUND``, the empty string,
  or ends in the suffix ``-NOTFOUND``.  Named boolean constants are
  case-insensitive.  If the argument is not one of these specific
diff --git a/Help/command/list.rst b/Help/command/list.rst
index 9b49cb4..33c4f80 100644
--- a/Help/command/list.rst
+++ b/Help/command/list.rst
@@ -128,7 +128,9 @@
 
   list(APPEND <list> [<element> ...])
 
-Appends elements to the list.
+Appends elements to the list. If no variable named ``<list>`` exists in the
+current scope its value is treated as empty and the elements are appended to
+that empty list.
 
 .. _FILTER:
 
@@ -150,7 +152,12 @@
 
   list(INSERT <list> <element_index> <element> [<element> ...])
 
-Inserts elements to the list to the specified location.
+Inserts elements to the list to the specified index. It is an
+error to specify an out-of-range index. Valid indexes are 0 to `N`
+where `N` is the length of the list, inclusive. An empty list
+has length 0. If no variable named ``<list>`` exists in the
+current scope its value is treated as empty and the elements are
+inserted in that empty list.
 
 .. _POP_BACK:
 
@@ -186,7 +193,9 @@
 
 .. versionadded:: 3.15
 
-Insert elements to the 0th position in the list.
+Insert elements to the 0th position in the list. If no variable named
+``<list>`` exists in the current scope its value is treated as empty and
+the elements are prepended to that empty list.
 
 .. _REMOVE_ITEM:
 
diff --git a/Help/manual/cmake-properties.7.rst b/Help/manual/cmake-properties.7.rst
index 5e18e10..ddb917a 100644
--- a/Help/manual/cmake-properties.7.rst
+++ b/Help/manual/cmake-properties.7.rst
@@ -191,6 +191,7 @@
    /prop_tgt/DEPLOYMENT_REMOTE_DIRECTORY
    /prop_tgt/DEPRECATION
    /prop_tgt/DISABLE_PRECOMPILE_HEADERS
+   /prop_tgt/DOTNET_SDK
    /prop_tgt/DOTNET_TARGET_FRAMEWORK
    /prop_tgt/DOTNET_TARGET_FRAMEWORK_VERSION
    /prop_tgt/EchoString
@@ -304,6 +305,7 @@
    /prop_tgt/LINK_INTERFACE_MULTIPLICITY
    /prop_tgt/LINK_INTERFACE_MULTIPLICITY_CONFIG
    /prop_tgt/LINK_LIBRARIES
+   /prop_tgt/LINK_LIBRARIES_ONLY_TARGETS
    /prop_tgt/LINK_OPTIONS
    /prop_tgt/LINK_SEARCH_END_STATIC
    /prop_tgt/LINK_SEARCH_START_STATIC
diff --git a/Help/manual/cmake-variables.7.rst b/Help/manual/cmake-variables.7.rst
index 3c50117..51b092f 100644
--- a/Help/manual/cmake-variables.7.rst
+++ b/Help/manual/cmake-variables.7.rst
@@ -49,6 +49,7 @@
    /variable/CMAKE_DEBUG_TARGET_PROPERTIES
    /variable/CMAKE_DIRECTORY_LABELS
    /variable/CMAKE_DL_LIBS
+   /variable/CMAKE_DOTNET_SDK
    /variable/CMAKE_DOTNET_TARGET_FRAMEWORK
    /variable/CMAKE_DOTNET_TARGET_FRAMEWORK_VERSION
    /variable/CMAKE_EDIT_COMMAND
@@ -220,6 +221,7 @@
    /variable/CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT
    /variable/CMAKE_LIBRARY_PATH
    /variable/CMAKE_LINK_DIRECTORIES_BEFORE
+   /variable/CMAKE_LINK_LIBRARIES_ONLY_TARGETS
    /variable/CMAKE_MFC_FLAG
    /variable/CMAKE_MAXIMUM_RECURSION_DEPTH
    /variable/CMAKE_MESSAGE_CONTEXT
diff --git a/Help/policy/CMP0028.rst b/Help/policy/CMP0028.rst
index ab38229..dcd39d8 100644
--- a/Help/policy/CMP0028.rst
+++ b/Help/policy/CMP0028.rst
@@ -13,6 +13,8 @@
 was considered to refer to a file on disk.  This can lead to confusing error
 messages if there is a typo in what should be a target name.
 
+See also the :prop_tgt:`LINK_LIBRARIES_ONLY_TARGETS` target property.
+
 The ``OLD`` behavior for this policy is to search for targets, then files on
 disk, even if the search term contains double-colons.  The ``NEW`` behavior
 for this policy is to issue a ``FATAL_ERROR`` if a link dependency contains
diff --git a/Help/prop_tgt/DOTNET_SDK.rst b/Help/prop_tgt/DOTNET_SDK.rst
new file mode 100644
index 0000000..ca1dcac
--- /dev/null
+++ b/Help/prop_tgt/DOTNET_SDK.rst
@@ -0,0 +1,25 @@
+DOTNET_SDK
+----------
+
+.. versionadded:: 3.23
+
+Specify the .NET SDK for C# projects.  For example: ``Microsoft.NET.Sdk``.
+
+This property tells :ref:`Visual Studio Generators` for VS 2019 and
+above to generate a .NET SDK-style project using the specified SDK.
+The property is meaningful only to these generators, and only in C#
+targets.  It is ignored for C++ projects, even if they are managed
+(e.g. using :prop_tgt:`COMMON_LANGUAGE_RUNTIME`).
+
+This property must be a non-empty string to generate .NET SDK-style projects.
+CMake does not perform any validations for the value of the property.
+
+This property may be initialized for all targets using the
+:variable:`CMAKE_DOTNET_SDK` variable.
+
+.. note::
+
+  The :ref:`Visual Studio Generators` in this version of CMake have not
+  yet learned to support :command:`add_custom_command` in .NET SDK-style
+  projects.  It is currently an error to attach a custom command to a
+  target with the ``DOTNET_SDK`` property set.
diff --git a/Help/prop_tgt/LINK_LIBRARIES_ONLY_TARGETS.rst b/Help/prop_tgt/LINK_LIBRARIES_ONLY_TARGETS.rst
new file mode 100644
index 0000000..78fbb02
--- /dev/null
+++ b/Help/prop_tgt/LINK_LIBRARIES_ONLY_TARGETS.rst
@@ -0,0 +1,55 @@
+LINK_LIBRARIES_ONLY_TARGETS
+---------------------------
+
+.. versionadded:: 3.23
+
+Enforce that link items that can be target names are actually existing targets.
+
+Set this property to a true value to enable additional checks on the contents
+of the :prop_tgt:`LINK_LIBRARIES` and :prop_tgt:`INTERFACE_LINK_LIBRARIES`
+target properties, typically populated by :command:`target_link_libraries`.
+CMake will verify that link items that might be target names actually name
+existing targets.  An item is considered a possible target name if:
+
+* it does not contain a ``/`` or ``\``, and
+* it does not start in ``-``, and
+* (for historical reasons) it does not start in ``$`` or `````.
+
+This property is initialized by the value of the
+:variable:`CMAKE_LINK_LIBRARIES_ONLY_TARGETS` variable when a non-imported
+target is created.  The property may be explicitly enabled on an imported
+target to check its link interface.
+
+For example, the following code:
+
+.. code-block:: cmake
+
+  set(CMAKE_LINK_LIBRARIES_ONLY_TARGETS ON)
+  add_executable(myLib STATIC myLib.c)
+  add_executable(myExe myExe.c)
+  target_link_libraries(myExe PRIVATE miLib) # typo for myLib
+
+will produce a CMake-time error that ``miLib`` is not a target.
+
+In order to link toolchain-provided libraries by name while still
+enforcing ``LINK_LIBRARIES_ONLY_TARGETS``, use an
+:ref:`imported <Imported Targets>`
+:ref:`Interface Library <Interface Libraries>` with the
+:prop_tgt:`IMPORTED_LIBNAME` target property:
+
+.. code-block:: cmake
+
+  add_library(toolchain::m INTERFACE IMPORTED)
+  set_property(TARGET toolchain::m PROPERTY IMPORTED_LIBNAME "m")
+  target_link_libraries(myExe PRIVATE toolchain::m)
+
+See also policy :policy:`CMP0028`.
+
+.. note::
+
+  If :prop_tgt:`INTERFACE_LINK_LIBRARIES` contains generator expressions,
+  its actual list of link items may depend on the type and properties of
+  the consuming target.  In such cases CMake may not always detect names
+  of missing targets that only appear for specific consumers.
+  A future version of CMake with improved heuristics may start triggering
+  errors on projects accepted by previous versions of CMake.
diff --git a/Help/release/dev/link-only-targets.rst b/Help/release/dev/link-only-targets.rst
new file mode 100644
index 0000000..7901a25
--- /dev/null
+++ b/Help/release/dev/link-only-targets.rst
@@ -0,0 +1,7 @@
+link-only-targets
+-----------------
+
+* The :variable:`CMAKE_LINK_LIBRARIES_ONLY_TARGETS` variable and
+  corresponding :prop_tgt:`LINK_LIBRARIES_ONLY_TARGETS` target
+  property were added to optionally require that all link items
+  that can be target names are actually names of existing targets.
diff --git a/Help/release/dev/vs-csharp-dotnet-sdk.rst b/Help/release/dev/vs-csharp-dotnet-sdk.rst
new file mode 100644
index 0000000..cc0ebe4
--- /dev/null
+++ b/Help/release/dev/vs-csharp-dotnet-sdk.rst
@@ -0,0 +1,9 @@
+vs-csharp-dotnet-sdk
+--------------------
+
+* The :ref:`Visual Studio Generators` for VS 2019 and above learned to
+  support .NET SDK-style project files (``.csproj``) for C# projects.
+  See the :prop_tgt:`DOTNET_SDK` target property and corresponding
+  :variable:`CMAKE_DOTNET_SDK` variable.
+  However, this version of CMake does not yet support using
+  :command:`add_custom_command` in .NET SDK-style projects.
diff --git a/Help/variable/CMAKE_DOTNET_SDK.rst b/Help/variable/CMAKE_DOTNET_SDK.rst
new file mode 100644
index 0000000..dc8806a
--- /dev/null
+++ b/Help/variable/CMAKE_DOTNET_SDK.rst
@@ -0,0 +1,9 @@
+CMAKE_DOTNET_SDK
+----------------
+
+.. versionadded:: 3.23
+
+Default value for :prop_tgt:`DOTNET_SDK` property of targets.
+
+This variable is used to initialize the :prop_tgt:`DOTNET_SDK`
+property on all targets. See that target property for additional information.
diff --git a/Help/variable/CMAKE_LINK_LIBRARIES_ONLY_TARGETS.rst b/Help/variable/CMAKE_LINK_LIBRARIES_ONLY_TARGETS.rst
new file mode 100644
index 0000000..513c3d0
--- /dev/null
+++ b/Help/variable/CMAKE_LINK_LIBRARIES_ONLY_TARGETS.rst
@@ -0,0 +1,10 @@
+CMAKE_LINK_LIBRARIES_ONLY_TARGETS
+---------------------------------
+
+.. versionadded:: 3.23
+
+Set this variable to initialize the :prop_tgt:`LINK_LIBRARIES_ONLY_TARGETS`
+property of non-imported targets when they are created.  Setting it to true
+enables an additional check that all items named by
+:command:`target_link_libraries` that can be target names are actually names
+of existing targets.  See the target property documentation for details.
diff --git a/Modules/Internal/CPack/CPackFreeBSD.cmake b/Modules/Internal/CPack/CPackFreeBSD.cmake
index ae40532..c35089c 100644
--- a/Modules/Internal/CPack/CPackFreeBSD.cmake
+++ b/Modules/Internal/CPack/CPackFreeBSD.cmake
@@ -34,7 +34,7 @@
     endif()
   endforeach()
   if(NOT VALUE)
-    message(WARNING "Variable ${OUTPUT_VAR_NAME} could not be given a fallback value from any variable ${FALLBACK_VAR_NAMES}.")
+    message(WARNING "Variable ${OUTPUT_VAR_NAME} could not be given a fallback value from (any of) ${FALLBACK_VAR_NAMES}.")
   endif()
 endfunction()
 
diff --git a/Source/CMakeVersion.cmake b/Source/CMakeVersion.cmake
index 86535b6..4b51017 100644
--- a/Source/CMakeVersion.cmake
+++ b/Source/CMakeVersion.cmake
@@ -1,7 +1,7 @@
 # CMake version number components.
 set(CMake_VERSION_MAJOR 3)
 set(CMake_VERSION_MINOR 22)
-set(CMake_VERSION_PATCH 20211222)
+set(CMake_VERSION_PATCH 20220106)
 #set(CMake_VERSION_RC 0)
 set(CMake_VERSION_IS_DIRTY 0)
 
diff --git a/Source/CPack/cmCPackFreeBSDGenerator.cxx b/Source/CPack/cmCPackFreeBSDGenerator.cxx
index fcd5753..b5d41fc 100644
--- a/Source/CPack/cmCPackFreeBSDGenerator.cxx
+++ b/Source/CPack/cmCPackFreeBSDGenerator.cxx
@@ -21,8 +21,15 @@
 
 #include <sys/stat.h>
 
+// Suffix used to tell libpkg what compression to use
+static const char FreeBSDPackageCompression[] = "txz";
+// Resulting package file-suffix, for < 1.17 and >= 1.17 versions of libpkg
+static const char FreeBSDPackageSuffix_10[] = ".txz";
+static const char FreeBSDPackageSuffix_17[] = ".pkg";
+
 cmCPackFreeBSDGenerator::cmCPackFreeBSDGenerator()
-  : cmCPackArchiveGenerator(cmArchiveWrite::CompressXZ, "paxr", ".txz")
+  : cmCPackArchiveGenerator(cmArchiveWrite::CompressXZ, "paxr",
+                            FreeBSDPackageSuffix_17)
 {
 }
 
@@ -35,6 +42,56 @@
 
 cmCPackFreeBSDGenerator::~cmCPackFreeBSDGenerator() = default;
 
+// This is a wrapper for struct pkg_create and pkg_create()
+//
+// Instantiate this class with suitable parameters, then
+// check isValid() to check if it's ok. Afterwards, call
+// Create() to do the actual work. This will leave a package
+// in the given `output_dir`.
+//
+// This wrapper cleans up the struct pkg_create.
+class PkgCreate
+{
+public:
+  PkgCreate()
+    : d(nullptr)
+  {
+  }
+  PkgCreate(const std::string& output_dir, const std::string& toplevel_dir,
+            const std::string& manifest_name)
+    : d(pkg_create_new())
+    , manifest(manifest_name)
+
+  {
+    if (d) {
+      pkg_create_set_format(d, FreeBSDPackageCompression);
+      pkg_create_set_compression_level(d, 0); // Explicitly set default
+      pkg_create_set_overwrite(d, false);
+      pkg_create_set_rootdir(d, toplevel_dir.c_str());
+      pkg_create_set_output_dir(d, output_dir.c_str());
+    }
+  }
+  ~PkgCreate()
+  {
+    if (d)
+      pkg_create_free(d);
+  }
+
+  bool isValid() const { return d; }
+
+  bool Create()
+  {
+    if (!isValid())
+      return false;
+    int r = pkg_create(d, manifest.c_str(), nullptr, false);
+    return r == 0;
+  }
+
+private:
+  struct pkg_create* d;
+  std::string manifest;
+};
+
 // This is a wrapper, for use only in stream-based output,
 // that will output a string in UCL escaped fashion (in particular,
 // quotes and backslashes are escaped). The list of characters
@@ -271,7 +328,7 @@
   s << "\"files\": {\n";
   for (std::string const& file : files) {
     s << "  \"/" << cmSystemTools::RelativePath(toplevel, file) << "\": \""
-      << "<sha256>"
+      << "<sha256>" // this gets replaced by libpkg by the actual SHA256
       << "\",\n";
   }
   s << "  },\n";
@@ -281,11 +338,10 @@
 {
   if (!this->ReadListFile("Internal/CPack/CPackFreeBSD.cmake")) {
     cmCPackLogger(cmCPackLog::LOG_ERROR,
-                  "Error while execution CPackFreeBSD.cmake" << std::endl);
+                  "Error while executing CPackFreeBSD.cmake" << std::endl);
     return 0;
   }
 
-  std::vector<std::string>::const_iterator fileIt;
   cmWorkingDirectory wd(toplevel);
 
   files.erase(std::remove_if(files.begin(), files.end(), ignore_file),
@@ -317,19 +373,84 @@
                              ONE_PACKAGE_PER_COMPONENT);
   }
 
-  std::string output_dir = cmSystemTools::CollapseFullPath("../", toplevel);
-  pkg_create_from_manifest(output_dir.c_str(), ::TXZ, toplevel.c_str(),
-                           manifestname.c_str(), nullptr);
+  // There should be one name in the packageFileNames (already, see comment
+  // in cmCPackGenerator::DoPackage(), which holds what CPack guesses
+  // will be the package filename. libpkg does something else, though,
+  // so update the single filename to what we know will be right.
+  if (this->packageFileNames.size() == 1) {
+    std::string currentPackage = this->packageFileNames[0];
+    auto lastSlash = currentPackage.rfind('/');
 
-  std::string broken_suffix =
-    cmStrCat('-', var_lookup("CPACK_TOPLEVEL_TAG"), ".txz");
+    // If there is a pathname, preserve that; libpkg will write out
+    // a file with the package name and version as specified in the
+    // manifest, so we look those up (again). lastSlash is the slash
+    // itself, we need that as path separator to the calculated package name.
+    std::string actualPackage =
+      ((lastSlash != std::string::npos)
+         ? std::string(currentPackage, 0, lastSlash + 1)
+         : std::string()) +
+      var_lookup("CPACK_FREEBSD_PACKAGE_NAME") + '-' +
+      var_lookup("CPACK_FREEBSD_PACKAGE_VERSION") + FreeBSDPackageSuffix_17;
+
+    this->packageFileNames.clear();
+    this->packageFileNames.emplace_back(actualPackage);
+  }
+
+  if (!pkg_initialized() && pkg_init(NULL, NULL) != EPKG_OK) {
+    cmCPackLogger(cmCPackLog::LOG_ERROR,
+                  "Can not initialize FreeBSD libpkg." << std::endl);
+    return 0;
+  }
+
+  std::string output_dir = cmSystemTools::CollapseFullPath("../", toplevel);
+  PkgCreate package(output_dir, toplevel, manifestname);
+  if (package.isValid()) {
+    if (!package.Create()) {
+      cmCPackLogger(cmCPackLog::LOG_ERROR,
+                    "Error during pkg_create()" << std::endl);
+      return 0;
+    }
+  } else {
+    cmCPackLogger(cmCPackLog::LOG_ERROR,
+                  "Error before pkg_create()" << std::endl);
+    return 0;
+  }
+
+  // Specifically looking for packages suffixed with the TAG, either extension
+  std::string broken_suffix_10 =
+    cmStrCat('-', var_lookup("CPACK_TOPLEVEL_TAG"), FreeBSDPackageSuffix_10);
+  std::string broken_suffix_17 =
+    cmStrCat('-', var_lookup("CPACK_TOPLEVEL_TAG"), FreeBSDPackageSuffix_17);
   for (std::string& name : packageFileNames) {
     cmCPackLogger(cmCPackLog::LOG_DEBUG, "Packagefile " << name << std::endl);
-    if (cmHasSuffix(name, broken_suffix)) {
-      name.replace(name.size() - broken_suffix.size(), std::string::npos,
-                   ".txz");
+    if (cmHasSuffix(name, broken_suffix_10)) {
+      name.replace(name.size() - broken_suffix_10.size(), std::string::npos,
+                   FreeBSDPackageSuffix_10);
       break;
     }
+    if (cmHasSuffix(name, broken_suffix_17)) {
+      name.replace(name.size() - broken_suffix_17.size(), std::string::npos,
+                   FreeBSDPackageSuffix_17);
+      break;
+    }
+  }
+  // If the name uses a *new* style name, which doesn't exist, but there
+  // is an *old* style name, then use that instead. This indicates we used
+  // an older libpkg, which still creates .txz instead of .pkg files.
+  for (std::string& name : packageFileNames) {
+    if (cmHasSuffix(name, FreeBSDPackageSuffix_17) &&
+        !cmSystemTools::FileExists(name)) {
+      const std::string badSuffix(FreeBSDPackageSuffix_17);
+      const std::string goodSuffix(FreeBSDPackageSuffix_10);
+      std::string repairedName(name);
+      repairedName.replace(repairedName.size() - badSuffix.size(),
+                           std::string::npos, goodSuffix);
+      if (cmSystemTools::FileExists(repairedName)) {
+        name = repairedName;
+        cmCPackLogger(cmCPackLog::LOG_DEBUG,
+                      "Repaired packagefile " << name << std::endl);
+      }
+    }
   }
 
   return 1;
diff --git a/Source/cmGeneratorTarget.cxx b/Source/cmGeneratorTarget.cxx
index e370472..421e136 100644
--- a/Source/cmGeneratorTarget.cxx
+++ b/Source/cmGeneratorTarget.cxx
@@ -6247,6 +6247,18 @@
 
 void cmGeneratorTarget::CheckLinkLibraries() const
 {
+  bool linkLibrariesOnlyTargets =
+    this->GetPropertyAsBool("LINK_LIBRARIES_ONLY_TARGETS");
+
+  // Evaluate the link interface of this target if needed for extra checks.
+  if (linkLibrariesOnlyTargets) {
+    std::vector<std::string> const& configs =
+      this->Makefile->GetGeneratorConfigs(cmMakefile::IncludeEmptyConfig);
+    for (std::string const& config : configs) {
+      this->GetLinkInterfaceLibraries(config, this, LinkInterfaceFor::Link);
+    }
+  }
+
   // Check link the implementation for each generated configuration.
   for (auto const& hmp : this->LinkImplMap) {
     HeadToLinkImplementationMap const& hm = hmp.second;
@@ -6260,6 +6272,10 @@
       if (!this->VerifyLinkItemColons(LinkItemRole::Implementation, item)) {
         return;
       }
+      if (linkLibrariesOnlyTargets &&
+          !this->VerifyLinkItemIsTarget(LinkItemRole::Implementation, item)) {
+        return;
+      }
     }
   }
 
@@ -6276,11 +6292,23 @@
         if (!this->VerifyLinkItemColons(LinkItemRole::Interface, item)) {
           return;
         }
+        if (linkLibrariesOnlyTargets &&
+            !this->VerifyLinkItemIsTarget(LinkItemRole::Interface, item)) {
+          return;
+        }
       }
     }
   }
 }
 
+namespace {
+cm::string_view missingTargetPossibleReasons =
+  "Possible reasons include:\n"
+  "  * There is a typo in the target name.\n"
+  "  * A find_package call is missing for an IMPORTED target.\n"
+  "  * An ALIAS target is missing.\n"_s;
+}
+
 bool cmGeneratorTarget::VerifyLinkItemColons(LinkItemRole role,
                                              cmLinkItem const& item) const
 {
@@ -6309,11 +6337,9 @@
     e = cmStrCat(e, "The link interface of target \"", this->GetName(),
                  "\" contains");
   }
-  e = cmStrCat(e, ":\n  ", item.AsStr(), "\n",
-               "but the target was not found.  Possible reasons include:\n"
-               "  * There is a typo in the target name.\n"
-               "  * A find_package call is missing for an IMPORTED target.\n"
-               "  * An ALIAS target is missing.\n");
+  e =
+    cmStrCat(e, ":\n  ", item.AsStr(), "\n", "but the target was not found.  ",
+             missingTargetPossibleReasons);
   cmListFileBacktrace backtrace = item.Backtrace;
   if (backtrace.Empty()) {
     backtrace = this->GetBacktrace();
@@ -6323,6 +6349,35 @@
   return false;
 }
 
+bool cmGeneratorTarget::VerifyLinkItemIsTarget(LinkItemRole role,
+                                               cmLinkItem const& item) const
+{
+  if (item.Target) {
+    return true;
+  }
+  std::string const& str = item.AsStr();
+  if (!str.empty() &&
+      (str[0] == '-' || str[0] == '$' || str[0] == '`' ||
+       str.find_first_of("/\\") != std::string::npos)) {
+    return true;
+  }
+
+  std::string e = cmStrCat("Target \"", this->GetName(),
+                           "\" has LINK_LIBRARIES_ONLY_TARGETS enabled, but ",
+                           role == LinkItemRole::Implementation
+                             ? "it links to"
+                             : "its link interface contains",
+                           ":\n  ", item.AsStr(), "\nwhich is not a target.  ",
+                           missingTargetPossibleReasons);
+  cmListFileBacktrace backtrace = item.Backtrace;
+  if (backtrace.Empty()) {
+    backtrace = this->GetBacktrace();
+  }
+  this->LocalGenerator->GetCMakeInstance()->IssueMessage(
+    MessageType::FATAL_ERROR, e, backtrace);
+  return false;
+}
+
 void cmGeneratorTarget::GetTargetVersion(int& major, int& minor) const
 {
   int patch;
@@ -7787,6 +7842,11 @@
   return languages.size() == 1 && languages.count("CSharp") > 0;
 }
 
+bool cmGeneratorTarget::IsDotNetSdkTarget() const
+{
+  return !this->GetProperty("DOTNET_SDK").IsEmpty();
+}
+
 void cmGeneratorTarget::ComputeLinkImplementationLanguages(
   const std::string& config, cmOptionalLinkImplementation& impl) const
 {
diff --git a/Source/cmGeneratorTarget.h b/Source/cmGeneratorTarget.h
index 096e2ea..71212c4 100644
--- a/Source/cmGeneratorTarget.h
+++ b/Source/cmGeneratorTarget.h
@@ -436,6 +436,8 @@
 
   bool IsCSharpOnly() const;
 
+  bool IsDotNetSdkTarget() const;
+
   void GetObjectLibrariesCMP0026(
     std::vector<cmGeneratorTarget*>& objlibs) const;
 
@@ -982,6 +984,7 @@
     Implementation,
     Interface,
   };
+  bool VerifyLinkItemIsTarget(LinkItemRole role, cmLinkItem const& item) const;
   bool VerifyLinkItemColons(LinkItemRole role, cmLinkItem const& item) const;
 
   // Cache import information from properties for each configuration.
diff --git a/Source/cmGlobalVisualStudio7Generator.cxx b/Source/cmGlobalVisualStudio7Generator.cxx
index b3f8d90..c72b109 100644
--- a/Source/cmGlobalVisualStudio7Generator.cxx
+++ b/Source/cmGlobalVisualStudio7Generator.cxx
@@ -373,8 +373,16 @@
         this->IsPartOfDefaultBuild(configs, projectTargets, target);
       cmValue vcprojName = target->GetProperty("GENERATOR_FILE_NAME");
       if (vcprojName) {
+        std::string mapping;
+
+        // On VS 19 and above, always map .NET SDK projects to "Any CPU".
+        if (target->IsDotNetSdkTarget() &&
+            this->GetVersion() >= VSVersion::VS16 &&
+            !this->IsReservedTarget(target->GetName())) {
+          mapping = "Any CPU";
+        }
         this->WriteProjectConfigurations(fout, *vcprojName, *target, configs,
-                                         configsPartOfDefaultBuild);
+                                         configsPartOfDefaultBuild, mapping);
       }
     }
   }
diff --git a/Source/cmMachO.h b/Source/cmMachO.h
index faa024b..ec7d54c 100644
--- a/Source/cmMachO.h
+++ b/Source/cmMachO.h
@@ -5,6 +5,7 @@
 #include "cmConfigure.h" // IWYU pragma: keep
 
 #include <iosfwd>
+#include <memory>
 #include <string>
 
 #if !defined(CMake_USE_MACH_PARSER)
diff --git a/Source/cmTarget.cxx b/Source/cmTarget.cxx
index 38361cc..c5703a1 100644
--- a/Source/cmTarget.cxx
+++ b/Source/cmTarget.cxx
@@ -471,6 +471,9 @@
         initProp(property);
       }
     }
+    if (!this->IsImported()) {
+      initProp("LINK_LIBRARIES_ONLY_TARGETS");
+    }
   }
 
   // Save the backtrace of target construction.
@@ -526,6 +529,10 @@
     this->impl->PolicyMap.Set(cmPolicies::CMP0022, cmPolicies::NEW);
   }
 
+  if (!this->IsImported()) {
+    initProp("DOTNET_SDK");
+  }
+
   if (this->impl->TargetType <= cmStateEnums::GLOBAL_TARGET) {
     initProp("DOTNET_TARGET_FRAMEWORK");
     initProp("DOTNET_TARGET_FRAMEWORK_VERSION");
diff --git a/Source/cmVisualStudio10TargetGenerator.cxx b/Source/cmVisualStudio10TargetGenerator.cxx
index 9523038..84044e4 100644
--- a/Source/cmVisualStudio10TargetGenerator.cxx
+++ b/Source/cmVisualStudio10TargetGenerator.cxx
@@ -405,6 +405,27 @@
   // Write the encoding header into the file
   char magic[] = { char(0xEF), char(0xBB), char(0xBF) };
   BuildFileStream.write(magic, 3);
+
+  if (this->Managed && this->ProjectType == VsProjectType::csproj &&
+      this->GeneratorTarget->IsDotNetSdkTarget() &&
+      this->GlobalGenerator->GetVersion() >=
+        cmGlobalVisualStudioGenerator::VS16) {
+    this->WriteSdkStyleProjectFile(BuildFileStream);
+  } else {
+    this->WriteClassicMsBuildProjectFile(BuildFileStream);
+  }
+
+  if (BuildFileStream.Close()) {
+    this->GlobalGenerator->FileReplacedDuringGenerate(PathToProjectFile);
+  }
+
+  // The groups are stored in a separate file for VS 10
+  this->WriteGroups();
+}
+
+void cmVisualStudio10TargetGenerator::WriteClassicMsBuildProjectFile(
+  cmGeneratedFileStream& BuildFileStream)
+{
   BuildFileStream << "<?xml version=\"1.0\" encoding=\""
                   << this->GlobalGenerator->Encoding() << "\"?>";
   {
@@ -447,14 +468,27 @@
       e1.Element("PreferredToolArchitecture", hostArch);
     }
 
+    // ALL_BUILD and ZERO_CHECK projects transitively include
+    // Microsoft.Common.CurrentVersion.targets which triggers Target
+    // ResolveNugetPackageAssets when SDK-style targets are in the project.
+    // However, these projects have no nuget packages to reference and the
+    // build fails.
+    // Setting ResolveNugetPackages to false skips this target and the build
+    // succeeds.
+    cm::string_view targetName{ this->GeneratorTarget->GetName() };
+    if (targetName == "ALL_BUILD" ||
+        targetName == CMAKE_CHECK_BUILD_SYSTEM_TARGET) {
+      Elem e1(e0, "PropertyGroup");
+      e1.Element("ResolveNugetPackages", "false");
+    }
+
     if (this->ProjectType != VsProjectType::csproj) {
       this->WriteProjectConfigurations(e0);
     }
 
     {
       Elem e1(e0, "PropertyGroup");
-      e1.Attribute("Label", "Globals");
-      e1.Element("ProjectGuid", "{" + this->GUID + "}");
+      this->WriteCommonPropertyGroupGlobals(e1);
 
       if ((this->MSTools || this->Android) &&
           this->GeneratorTarget->IsInBuildSystem()) {
@@ -462,16 +496,6 @@
         this->VerifyNecessaryFiles();
       }
 
-      cmValue vsProjectTypes =
-        this->GeneratorTarget->GetProperty("VS_GLOBAL_PROJECT_TYPES");
-      if (vsProjectTypes) {
-        const char* tagName = "ProjectTypes";
-        if (this->ProjectType == VsProjectType::csproj) {
-          tagName = "ProjectTypeGuids";
-        }
-        e1.Element(tagName, *vsProjectTypes);
-      }
-
       cmValue vsProjectName =
         this->GeneratorTarget->GetProperty("VS_SCC_PROJECTNAME");
       cmValue vsLocalPath =
@@ -495,24 +519,6 @@
         e1.Element("WinMDAssembly", "true");
       }
 
-      cmValue vsGlobalKeyword =
-        this->GeneratorTarget->GetProperty("VS_GLOBAL_KEYWORD");
-      if (!vsGlobalKeyword) {
-        if (this->GlobalGenerator->TargetsAndroid()) {
-          e1.Element("Keyword", "Android");
-        } else {
-          e1.Element("Keyword", "Win32Proj");
-        }
-      } else {
-        e1.Element("Keyword", *vsGlobalKeyword);
-      }
-
-      cmValue vsGlobalRootNamespace =
-        this->GeneratorTarget->GetProperty("VS_GLOBAL_ROOTNAMESPACE");
-      if (vsGlobalRootNamespace) {
-        e1.Element("RootNamespace", *vsGlobalRootNamespace);
-      }
-
       e1.Element("Platform", this->Platform);
       cmValue projLabel = this->GeneratorTarget->GetProperty("PROJECT_LABEL");
       e1.Element("ProjectName", projLabel ? projLabel : this->Name);
@@ -602,24 +608,6 @@
         e1.Element("VCTargetsPath", vcTargetsPath);
       }
 
-      std::vector<std::string> keys = this->GeneratorTarget->GetPropertyKeys();
-      for (std::string const& keyIt : keys) {
-        static const cm::string_view prefix = "VS_GLOBAL_";
-        if (!cmHasPrefix(keyIt, prefix))
-          continue;
-        cm::string_view globalKey =
-          cm::string_view(keyIt).substr(prefix.length());
-        // Skip invalid or separately-handled properties.
-        if (globalKey.empty() || globalKey == "PROJECT_TYPES" ||
-            globalKey == "ROOTNAMESPACE" || globalKey == "KEYWORD") {
-          continue;
-        }
-        cmValue value = this->GeneratorTarget->GetProperty(keyIt);
-        if (!value)
-          continue;
-        e1.Element(globalKey, *value);
-      }
-
       if (this->Managed) {
         if (this->LocalGenerator->GetVersion() >=
             cmGlobalVisualStudioGenerator::VS17) {
@@ -839,13 +827,165 @@
       }
     }
   }
+}
 
-  if (BuildFileStream.Close()) {
-    this->GlobalGenerator->FileReplacedDuringGenerate(PathToProjectFile);
+void cmVisualStudio10TargetGenerator::WriteSdkStyleProjectFile(
+  cmGeneratedFileStream& BuildFileStream)
+{
+  if (!this->Managed || this->ProjectType != VsProjectType::csproj ||
+      !this->GeneratorTarget->IsDotNetSdkTarget()) {
+    std::string message = "The target \"" + this->GeneratorTarget->GetName() +
+      "\" is not eligible for .Net SDK style project.";
+    this->Makefile->IssueMessage(MessageType::INTERNAL_ERROR, message);
+    return;
   }
 
-  // The groups are stored in a separate file for VS 10
-  this->WriteGroups();
+  if (this->HasCustomCommands()) {
+    std::string message = "The target \"" + this->GeneratorTarget->GetName() +
+      "\" does not currently support add_custom_command as the Visual Studio "
+      "generators have not yet learned how to generate custom commands in "
+      ".Net SDK-style projects.";
+    this->Makefile->IssueMessage(MessageType::FATAL_ERROR, message);
+    return;
+  }
+
+  Elem e0(BuildFileStream, "Project");
+  e0.Attribute("Sdk", *this->GeneratorTarget->GetProperty("DOTNET_SDK"));
+
+  {
+    Elem e1(e0, "PropertyGroup");
+    this->WriteCommonPropertyGroupGlobals(e1);
+
+    e1.Element("EnableDefaultItems", "false");
+    // Disable the project upgrade prompt that is displayed the first time a
+    // project using an older toolset version is opened in a newer version
+    // of the IDE.
+    e1.Element("VCProjectUpgraderObjectName", "NoUpgrade");
+    e1.Element("ManagedAssembly", "true");
+
+    cmValue targetFramework =
+      this->GeneratorTarget->GetProperty("DOTNET_TARGET_FRAMEWORK");
+    if (targetFramework) {
+      if (targetFramework->find(';') != std::string::npos) {
+        e1.Element("TargetFrameworks", *targetFramework);
+      } else {
+        e1.Element("TargetFramework", *targetFramework);
+      }
+    } else {
+      e1.Element("TargetFramework", "net5.0");
+    }
+
+    std::string outputType;
+    switch (this->GeneratorTarget->GetType()) {
+      case cmStateEnums::OBJECT_LIBRARY:
+      case cmStateEnums::STATIC_LIBRARY:
+      case cmStateEnums::MODULE_LIBRARY:
+        this->Makefile->IssueMessage(
+          MessageType::FATAL_ERROR,
+          cmStrCat("Target \"", this->GeneratorTarget->GetName(),
+                   "\" is of a type not supported for managed binaries."));
+        return;
+      case cmStateEnums::SHARED_LIBRARY:
+        outputType = "Library";
+        break;
+      case cmStateEnums::EXECUTABLE: {
+        auto const win32 =
+          this->GeneratorTarget->GetSafeProperty("WIN32_EXECUTABLE");
+        if (win32.find("$<") != std::string::npos) {
+          this->Makefile->IssueMessage(
+            MessageType::FATAL_ERROR,
+            cmStrCat("Target \"", this->GeneratorTarget->GetName(),
+                     "\" has a generator expression in its WIN32_EXECUTABLE "
+                     "property. This is not supported on managed "
+                     "executables."));
+          return;
+        }
+        outputType = "Exe";
+      } break;
+      case cmStateEnums::UTILITY:
+      case cmStateEnums::INTERFACE_LIBRARY:
+      case cmStateEnums::GLOBAL_TARGET:
+        outputType = "Utility";
+        break;
+      case cmStateEnums::UNKNOWN_LIBRARY:
+        break;
+    }
+    e1.Element("OutputType", outputType);
+  }
+
+  this->WriteDotNetDocumentationFile(e0);
+  this->WriteAllSources(e0);
+  this->WritePackageReferences(e0);
+  this->WriteProjectReferences(e0);
+}
+
+void cmVisualStudio10TargetGenerator::WriteCommonPropertyGroupGlobals(Elem& e1)
+{
+  e1.Attribute("Label", "Globals");
+  e1.Element("ProjectGuid", "{" + this->GUID + "}");
+
+  cmValue vsProjectTypes =
+    this->GeneratorTarget->GetProperty("VS_GLOBAL_PROJECT_TYPES");
+  if (vsProjectTypes) {
+    const char* tagName = "ProjectTypes";
+    if (this->ProjectType == VsProjectType::csproj) {
+      tagName = "ProjectTypeGuids";
+    }
+    e1.Element(tagName, *vsProjectTypes);
+  }
+
+  cmValue vsGlobalKeyword =
+    this->GeneratorTarget->GetProperty("VS_GLOBAL_KEYWORD");
+  if (!vsGlobalKeyword) {
+    if (this->GlobalGenerator->TargetsAndroid()) {
+      e1.Element("Keyword", "Android");
+    } else {
+      e1.Element("Keyword", "Win32Proj");
+    }
+  } else {
+    e1.Element("Keyword", *vsGlobalKeyword);
+  }
+
+  cmValue vsGlobalRootNamespace =
+    this->GeneratorTarget->GetProperty("VS_GLOBAL_ROOTNAMESPACE");
+  if (vsGlobalRootNamespace) {
+    e1.Element("RootNamespace", *vsGlobalRootNamespace);
+  }
+
+  std::vector<std::string> keys = this->GeneratorTarget->GetPropertyKeys();
+  for (std::string const& keyIt : keys) {
+    static const cm::string_view prefix = "VS_GLOBAL_";
+    if (!cmHasPrefix(keyIt, prefix))
+      continue;
+    cm::string_view globalKey = cm::string_view(keyIt).substr(prefix.length());
+    // Skip invalid or separately-handled properties.
+    if (globalKey.empty() || globalKey == "PROJECT_TYPES" ||
+        globalKey == "ROOTNAMESPACE" || globalKey == "KEYWORD") {
+      continue;
+    }
+    cmValue value = this->GeneratorTarget->GetProperty(keyIt);
+    if (!value)
+      continue;
+    e1.Element(globalKey, *value);
+  }
+}
+
+bool cmVisualStudio10TargetGenerator::HasCustomCommands() const
+{
+  if (!this->GeneratorTarget->GetPreBuildCommands().empty() ||
+      !this->GeneratorTarget->GetPreLinkCommands().empty() ||
+      !this->GeneratorTarget->GetPostBuildCommands().empty()) {
+    return true;
+  }
+
+  for (cmGeneratorTarget::AllConfigSource const& si :
+       this->GeneratorTarget->GetAllConfigSources()) {
+    if (si.Source->GetCustomCommand()) {
+      return true;
+    }
+  }
+
+  return false;
 }
 
 void cmVisualStudio10TargetGenerator::WritePackageReferences(Elem& e0)
diff --git a/Source/cmVisualStudio10TargetGenerator.h b/Source/cmVisualStudio10TargetGenerator.h
index ec6362f..37b8dfd 100644
--- a/Source/cmVisualStudio10TargetGenerator.h
+++ b/Source/cmVisualStudio10TargetGenerator.h
@@ -18,6 +18,7 @@
 class cmComputeLinkInformation;
 class cmCustomCommand;
 class cmCustomCommandGenerator;
+class cmGeneratedFileStream;
 class cmGlobalVisualStudio10Generator;
 class cmLocalVisualStudio10Generator;
 class cmMakefile;
@@ -260,6 +261,15 @@
   void ClassifyAllConfigSources();
   void ClassifyAllConfigSource(cmGeneratorTarget::AllConfigSource const& acs);
 
+  // .Net SDK-stype project variable and helper functions
+  void WriteClassicMsBuildProjectFile(cmGeneratedFileStream& BuildFileStream);
+  void WriteSdkStyleProjectFile(cmGeneratedFileStream& BuildFileStream);
+
+  void WriteCommonPropertyGroupGlobals(
+    cmVisualStudio10TargetGenerator::Elem& e1);
+
+  bool HasCustomCommands() const;
+
   std::unordered_map<std::string, ConfigToSettings> ParsedToolTargetSettings;
   bool PropertyIsSameInAllConfigs(const ConfigToSettings& toolSettings,
                                   const std::string& propName);
diff --git a/Tests/RunCMake/CMakeLists.txt b/Tests/RunCMake/CMakeLists.txt
index 5dc7031..f0457a8 100644
--- a/Tests/RunCMake/CMakeLists.txt
+++ b/Tests/RunCMake/CMakeLists.txt
@@ -94,7 +94,6 @@
 add_RunCMake_test(CMP0022)
 add_RunCMake_test(CMP0026)
 add_RunCMake_test(CMP0027)
-add_RunCMake_test(CMP0028)
 add_RunCMake_test(CMP0037)
 add_RunCMake_test(CMP0038)
 add_RunCMake_test(CMP0039)
@@ -329,6 +328,7 @@
 add_RunCMake_test(Graphviz)
 add_RunCMake_test(TargetPropertyGeneratorExpressions)
 add_RunCMake_test(Languages)
+add_RunCMake_test(LinkItemValidation)
 add_RunCMake_test(LinkStatic)
 if(CMAKE_CXX_COMPILER_ID MATCHES "^(Cray|PGI|NVHPC|XL|XLClang|Fujitsu|FujitsuClang)$")
   add_RunCMake_test(MetaCompileFeatures)
@@ -616,6 +616,10 @@
   endif()
 endif()
 
+if(CMAKE_GENERATOR MATCHES "^Visual Studio (1[6-9]|[2-9][0-9])")
+  add_RunCMake_test(VsDotnetSdk)
+endif()
+
 if(XCODE_VERSION)
   add_RunCMake_test(XcodeProject -DXCODE_VERSION=${XCODE_VERSION})
   add_RunCMake_test(XcodeProject-Embed -DXCODE_VERSION=${XCODE_VERSION})
diff --git a/Tests/RunCMake/CMP0028/CMP0028-NEW-iface-result.txt b/Tests/RunCMake/LinkItemValidation/CMP0028-NEW-iface-result.txt
similarity index 100%
rename from Tests/RunCMake/CMP0028/CMP0028-NEW-iface-result.txt
rename to Tests/RunCMake/LinkItemValidation/CMP0028-NEW-iface-result.txt
diff --git a/Tests/RunCMake/CMP0028/CMP0028-NEW-iface-stderr.txt b/Tests/RunCMake/LinkItemValidation/CMP0028-NEW-iface-stderr.txt
similarity index 100%
rename from Tests/RunCMake/CMP0028/CMP0028-NEW-iface-stderr.txt
rename to Tests/RunCMake/LinkItemValidation/CMP0028-NEW-iface-stderr.txt
diff --git a/Tests/RunCMake/CMP0028/CMP0028-NEW-iface.cmake b/Tests/RunCMake/LinkItemValidation/CMP0028-NEW-iface.cmake
similarity index 100%
rename from Tests/RunCMake/CMP0028/CMP0028-NEW-iface.cmake
rename to Tests/RunCMake/LinkItemValidation/CMP0028-NEW-iface.cmake
diff --git a/Tests/RunCMake/CMP0028/CMP0028-NEW-result.txt b/Tests/RunCMake/LinkItemValidation/CMP0028-NEW-result.txt
similarity index 100%
rename from Tests/RunCMake/CMP0028/CMP0028-NEW-result.txt
rename to Tests/RunCMake/LinkItemValidation/CMP0028-NEW-result.txt
diff --git a/Tests/RunCMake/CMP0028/CMP0028-NEW-stderr.txt b/Tests/RunCMake/LinkItemValidation/CMP0028-NEW-stderr.txt
similarity index 100%
rename from Tests/RunCMake/CMP0028/CMP0028-NEW-stderr.txt
rename to Tests/RunCMake/LinkItemValidation/CMP0028-NEW-stderr.txt
diff --git a/Tests/RunCMake/CMP0028/CMP0028-NEW.cmake b/Tests/RunCMake/LinkItemValidation/CMP0028-NEW.cmake
similarity index 100%
rename from Tests/RunCMake/CMP0028/CMP0028-NEW.cmake
rename to Tests/RunCMake/LinkItemValidation/CMP0028-NEW.cmake
diff --git a/Tests/RunCMake/CMP0028/CMP0028-OLD-iface-result.txt b/Tests/RunCMake/LinkItemValidation/CMP0028-OLD-iface-result.txt
similarity index 100%
rename from Tests/RunCMake/CMP0028/CMP0028-OLD-iface-result.txt
rename to Tests/RunCMake/LinkItemValidation/CMP0028-OLD-iface-result.txt
diff --git a/Tests/RunCMake/CMP0028/CMP0028-OLD-iface-stderr.txt b/Tests/RunCMake/LinkItemValidation/CMP0028-OLD-iface-stderr.txt
similarity index 100%
rename from Tests/RunCMake/CMP0028/CMP0028-OLD-iface-stderr.txt
rename to Tests/RunCMake/LinkItemValidation/CMP0028-OLD-iface-stderr.txt
diff --git a/Tests/RunCMake/CMP0028/CMP0028-OLD-iface.cmake b/Tests/RunCMake/LinkItemValidation/CMP0028-OLD-iface.cmake
similarity index 100%
rename from Tests/RunCMake/CMP0028/CMP0028-OLD-iface.cmake
rename to Tests/RunCMake/LinkItemValidation/CMP0028-OLD-iface.cmake
diff --git a/Tests/RunCMake/CMP0028/CMP0028-OLD-result.txt b/Tests/RunCMake/LinkItemValidation/CMP0028-OLD-result.txt
similarity index 100%
rename from Tests/RunCMake/CMP0028/CMP0028-OLD-result.txt
rename to Tests/RunCMake/LinkItemValidation/CMP0028-OLD-result.txt
diff --git a/Tests/RunCMake/CMP0028/CMP0028-OLD-stderr.txt b/Tests/RunCMake/LinkItemValidation/CMP0028-OLD-stderr.txt
similarity index 100%
rename from Tests/RunCMake/CMP0028/CMP0028-OLD-stderr.txt
rename to Tests/RunCMake/LinkItemValidation/CMP0028-OLD-stderr.txt
diff --git a/Tests/RunCMake/CMP0028/CMP0028-OLD.cmake b/Tests/RunCMake/LinkItemValidation/CMP0028-OLD.cmake
similarity index 100%
rename from Tests/RunCMake/CMP0028/CMP0028-OLD.cmake
rename to Tests/RunCMake/LinkItemValidation/CMP0028-OLD.cmake
diff --git a/Tests/RunCMake/CMP0028/CMP0028-WARN-iface-result.txt b/Tests/RunCMake/LinkItemValidation/CMP0028-WARN-iface-result.txt
similarity index 100%
rename from Tests/RunCMake/CMP0028/CMP0028-WARN-iface-result.txt
rename to Tests/RunCMake/LinkItemValidation/CMP0028-WARN-iface-result.txt
diff --git a/Tests/RunCMake/CMP0028/CMP0028-WARN-iface-stderr.txt b/Tests/RunCMake/LinkItemValidation/CMP0028-WARN-iface-stderr.txt
similarity index 100%
rename from Tests/RunCMake/CMP0028/CMP0028-WARN-iface-stderr.txt
rename to Tests/RunCMake/LinkItemValidation/CMP0028-WARN-iface-stderr.txt
diff --git a/Tests/RunCMake/CMP0028/CMP0028-WARN-iface.cmake b/Tests/RunCMake/LinkItemValidation/CMP0028-WARN-iface.cmake
similarity index 100%
rename from Tests/RunCMake/CMP0028/CMP0028-WARN-iface.cmake
rename to Tests/RunCMake/LinkItemValidation/CMP0028-WARN-iface.cmake
diff --git a/Tests/RunCMake/CMP0028/CMP0028-WARN-result.txt b/Tests/RunCMake/LinkItemValidation/CMP0028-WARN-result.txt
similarity index 100%
rename from Tests/RunCMake/CMP0028/CMP0028-WARN-result.txt
rename to Tests/RunCMake/LinkItemValidation/CMP0028-WARN-result.txt
diff --git a/Tests/RunCMake/CMP0028/CMP0028-WARN-stderr.txt b/Tests/RunCMake/LinkItemValidation/CMP0028-WARN-stderr.txt
similarity index 100%
rename from Tests/RunCMake/CMP0028/CMP0028-WARN-stderr.txt
rename to Tests/RunCMake/LinkItemValidation/CMP0028-WARN-stderr.txt
diff --git a/Tests/RunCMake/CMP0028/CMP0028-WARN.cmake b/Tests/RunCMake/LinkItemValidation/CMP0028-WARN.cmake
similarity index 100%
rename from Tests/RunCMake/CMP0028/CMP0028-WARN.cmake
rename to Tests/RunCMake/LinkItemValidation/CMP0028-WARN.cmake
diff --git a/Tests/RunCMake/CMP0028/CMakeLists.txt b/Tests/RunCMake/LinkItemValidation/CMakeLists.txt
similarity index 62%
rename from Tests/RunCMake/CMP0028/CMakeLists.txt
rename to Tests/RunCMake/LinkItemValidation/CMakeLists.txt
index 4f867df..185cd91 100644
--- a/Tests/RunCMake/CMP0028/CMakeLists.txt
+++ b/Tests/RunCMake/LinkItemValidation/CMakeLists.txt
@@ -1,3 +1,6 @@
 cmake_minimum_required(VERSION 2.8.12)
+if(NOT RunCMake_TEST MATCHES "^CMP0028")
+  cmake_minimum_required(VERSION 3.22)
+endif()
 project(${RunCMake_TEST} CXX)
 include(${RunCMake_TEST}.cmake NO_POLICY_SCOPE) # policy used at end of dir
diff --git a/Tests/RunCMake/CMP0028/CMP0028-NEW-result.txt b/Tests/RunCMake/LinkItemValidation/OnlyTargets-result.txt
similarity index 100%
copy from Tests/RunCMake/CMP0028/CMP0028-NEW-result.txt
copy to Tests/RunCMake/LinkItemValidation/OnlyTargets-result.txt
diff --git a/Tests/RunCMake/LinkItemValidation/OnlyTargets-stderr.txt b/Tests/RunCMake/LinkItemValidation/OnlyTargets-stderr.txt
new file mode 100644
index 0000000..bbb0170
--- /dev/null
+++ b/Tests/RunCMake/LinkItemValidation/OnlyTargets-stderr.txt
@@ -0,0 +1,40 @@
+^CMake Error at OnlyTargets\.cmake:11 \(target_link_libraries\):
+  Target "exe" has LINK_LIBRARIES_ONLY_TARGETS enabled, but it links to:
+
+    non_target_in_exe
+
+  which is not a target\.  Possible reasons include:
+(
+    \*[^
+]+)*
+
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)
++
+CMake Error at OnlyTargets\.cmake:21 \(target_link_libraries\):
+  Target "iface" has LINK_LIBRARIES_ONLY_TARGETS enabled, but its link
+  interface contains:
+
+    non_target_in_iface
+
+  which is not a target\.  Possible reasons include:
+(
+    \*[^
+]+)*
+
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)
++
+CMake Error at OnlyTargets\.cmake:30 \(target_link_libraries\):
+  Target "iface_imported_checked" has LINK_LIBRARIES_ONLY_TARGETS enabled,
+  but its link interface contains:
+
+    non_target_in_iface_imported_checked
+
+  which is not a target\.  Possible reasons include:
+(
+    \*[^
+]+)*
+
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)
diff --git a/Tests/RunCMake/LinkItemValidation/OnlyTargets.cmake b/Tests/RunCMake/LinkItemValidation/OnlyTargets.cmake
new file mode 100644
index 0000000..9417318
--- /dev/null
+++ b/Tests/RunCMake/LinkItemValidation/OnlyTargets.cmake
@@ -0,0 +1,56 @@
+enable_language(C)
+
+set(CMAKE_LINK_LIBRARIES_ONLY_TARGETS 1)
+
+# Use imported interface library to name toolchain-provided libraries.
+add_library(toolchain::m INTERFACE IMPORTED)
+set_property(TARGET toolchain::m PROPERTY IMPORTED_LIBNAME "m")
+
+# Linking directly warns.
+add_executable(exe main.c)
+target_link_libraries(exe PRIVATE
+  -lflag_in_exe     # accepted
+  /abs/path/in_exe  # accepted
+  rel/path/in_exe   # accepted
+  toolchain::m      # accepted
+  non_target_in_exe # rejected
+  )
+
+# Link interfaces warn.
+add_library(iface INTERFACE)
+target_link_libraries(iface INTERFACE
+  -lflag_in_iface     # accepted
+  /abs/path/in_iface  # accepted
+  rel/path/in_iface   # accepted
+  non_target_in_iface # rejected
+  )
+
+# Imported target link interfaces warn if explicitly enabled.
+add_library(iface_imported_checked INTERFACE IMPORTED)
+target_link_libraries(iface_imported_checked INTERFACE
+  -lflag_iface_imported_checked        # accepted
+  /abs/path/in_iface_imported_checked  # accepted
+  rel/path/in_iface_imported_checked   # accepted
+  non_target_in_iface_imported_checked # rejected
+  )
+set_property(TARGET iface_imported_checked PROPERTY LINK_LIBRARIES_ONLY_TARGETS 1)
+
+# Linking directly does not warn if explicitly disabled.
+add_executable(exe_not_checked main.c)
+target_link_libraries(exe_not_checked PRIVATE
+  non_target_in_exe_not_checked
+  )
+set_property(TARGET exe_not_checked PROPERTY LINK_LIBRARIES_ONLY_TARGETS 0)
+
+# Link interfaces do not warn if explicitly disabled.
+add_library(iface_not_checked INTERFACE)
+target_link_libraries(iface_not_checked INTERFACE
+  non_target_in_iface_not_checked
+  )
+set_property(TARGET iface_not_checked PROPERTY LINK_LIBRARIES_ONLY_TARGETS 0)
+
+# Imported target link interfaces do not warn if not explicitly enabled.
+add_library(iface_imported_default INTERFACE IMPORTED)
+target_link_libraries(iface_imported_default INTERFACE
+  non_target_in_iface_imported_default
+  )
diff --git a/Tests/RunCMake/CMP0028/RunCMakeTest.cmake b/Tests/RunCMake/LinkItemValidation/RunCMakeTest.cmake
similarity index 87%
rename from Tests/RunCMake/CMP0028/RunCMakeTest.cmake
rename to Tests/RunCMake/LinkItemValidation/RunCMakeTest.cmake
index 0c72ca2..c423f6a 100644
--- a/Tests/RunCMake/CMP0028/RunCMakeTest.cmake
+++ b/Tests/RunCMake/LinkItemValidation/RunCMakeTest.cmake
@@ -6,3 +6,5 @@
 run_cmake(CMP0028-NEW-iface)
 run_cmake(CMP0028-OLD-iface)
 run_cmake(CMP0028-WARN-iface)
+
+run_cmake(OnlyTargets)
diff --git a/Tests/RunCMake/CMP0028/empty.cpp b/Tests/RunCMake/LinkItemValidation/empty.cpp
similarity index 100%
rename from Tests/RunCMake/CMP0028/empty.cpp
rename to Tests/RunCMake/LinkItemValidation/empty.cpp
diff --git a/Tests/RunCMake/LinkItemValidation/main.c b/Tests/RunCMake/LinkItemValidation/main.c
new file mode 100644
index 0000000..8488f4e
--- /dev/null
+++ b/Tests/RunCMake/LinkItemValidation/main.c
@@ -0,0 +1,4 @@
+int main(void)
+{
+  return 0;
+}
diff --git a/Tests/RunCMake/VsDotnetSdk/CMakeLists.txt b/Tests/RunCMake/VsDotnetSdk/CMakeLists.txt
new file mode 100644
index 0000000..e597708
--- /dev/null
+++ b/Tests/RunCMake/VsDotnetSdk/CMakeLists.txt
@@ -0,0 +1,3 @@
+cmake_minimum_required(VERSION 3.22.0)
+project(${RunCMake_TEST} NONE)
+include(${RunCMake_TEST}.cmake)
diff --git a/Tests/RunCMake/VsDotnetSdk/DotnetSdkVariables-check.cmake b/Tests/RunCMake/VsDotnetSdk/DotnetSdkVariables-check.cmake
new file mode 100644
index 0000000..7a5cd1d
--- /dev/null
+++ b/Tests/RunCMake/VsDotnetSdk/DotnetSdkVariables-check.cmake
@@ -0,0 +1,52 @@
+set(files foo.csproj bar.csproj baz.csproj)
+
+set(inLib1 FALSE)
+set(dotnetSdkInLib1 FALSE)
+
+set(inLib2 FALSE)
+set(dotnetSdkWebInLib2 FALSE)
+
+set(inLib3 FALSE)
+set(classicProjInLib3 FALSE)
+
+foreach(file ${files})
+  set(csProjectFile ${RunCMake_TEST_BINARY_DIR}/${file})
+
+  if(NOT EXISTS "${csProjectFile}")
+    set(RunCMake_TEST_FAILED "Project file ${csProjectFile} does not exist.")
+    return()
+  endif()
+
+  file(STRINGS "${csProjectFile}" lines)
+
+  foreach(line IN LISTS lines)
+    if(NOT inLib1)
+      if(line MATCHES "<Project Sdk=\"Microsoft\.NET\.Sdk\">")
+        set(dotnetSdkInLib1 TRUE)
+        set(inLib1  TRUE)
+      endif()
+    elseif(NOT inLib2)
+      if(line MATCHES "<Project Sdk=\"Microsoft\.NET\.Sdk\.Web\">")
+        set(dotnetSdkWebInLib2 TRUE)
+        set(inLib2 TRUE)
+      endif()
+    elseif(NOT inLib3)
+      if(line MATCHES "<Project DefaultTargets=\"Build\" ToolsVersion=\"")
+        set(classicProjInLib3 TRUE)
+        set(inLib3 TRUE)
+      endif()
+    endif()
+  endforeach()
+endforeach()
+
+if(NOT dotnetSdkInLib1)
+  set(RunCMake_TEST_FAILED ".Net SDK not set correctly.")
+endif()
+
+if(NOT dotnetSdkWebInLib2)
+  set(RunCMake_TEST_FAILED ".Net Web SDK not set correctly.")
+endif()
+
+if(NOT classicProjInLib3)
+  set(RunCMake_TEST_FAILED "Empty DOTNET_SDK doesn't build Classic project.")
+endif()
diff --git a/Tests/RunCMake/VsDotnetSdk/DotnetSdkVariables.cmake b/Tests/RunCMake/VsDotnetSdk/DotnetSdkVariables.cmake
new file mode 100644
index 0000000..f080edd
--- /dev/null
+++ b/Tests/RunCMake/VsDotnetSdk/DotnetSdkVariables.cmake
@@ -0,0 +1,14 @@
+enable_language(CSharp)
+
+if(NOT CMAKE_CSharp_COMPILER)
+    return()
+endif()
+
+set(CMAKE_DOTNET_SDK "Microsoft.NET.Sdk")
+add_library(foo SHARED lib1.cs)
+
+set(CMAKE_DOTNET_SDK "Microsoft.NET.Sdk.Web")
+add_library(bar SHARED lib1.cs)
+
+set(CMAKE_DOTNET_SDK "")
+add_library(baz SHARED lib1.cs)
diff --git a/Tests/RunCMake/VsDotnetSdk/RunCMakeTest.cmake b/Tests/RunCMake/VsDotnetSdk/RunCMakeTest.cmake
new file mode 100644
index 0000000..b174c25
--- /dev/null
+++ b/Tests/RunCMake/VsDotnetSdk/RunCMakeTest.cmake
@@ -0,0 +1,17 @@
+cmake_policy(SET CMP0053 NEW)
+include(RunCMake)
+
+run_cmake(VsDotnetSdkCustomCommandsTarget)
+run_cmake(VsDotnetSdkCustomCommandsSource)
+run_cmake(DotnetSdkVariables)
+
+function(run_VsDotnetSdk)
+  set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/VsDotnetSdk-build)
+  set(RunCMake_TEST_NO_CLEAN 1)
+  file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}")
+  file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}")
+  run_cmake(VsDotnetSdk)
+  set(build_flags /restore)
+  run_cmake_command(VsDotnetSdk-build ${CMAKE_COMMAND} --build . -- ${build_flags})
+endfunction()
+run_VsDotnetSdk()
diff --git a/Tests/RunCMake/VsDotnetSdk/VsDotnetSdk.cmake b/Tests/RunCMake/VsDotnetSdk/VsDotnetSdk.cmake
new file mode 100644
index 0000000..60066ab
--- /dev/null
+++ b/Tests/RunCMake/VsDotnetSdk/VsDotnetSdk.cmake
@@ -0,0 +1,18 @@
+cmake_minimum_required(VERSION 3.22)
+
+# a simple CSharp only test case
+project (DotNetSdk CSharp)
+
+set(CMAKE_DOTNET_TARGET_FRAMEWORK net472)
+set(CMAKE_DOTNET_SDK "Microsoft.NET.Sdk")
+
+add_library(dotNetSdkLib1 SHARED lib1.cs)
+set_target_properties(dotNetSdkLib1
+    PROPERTIES
+        VS_GLOBAL_RuntimeIdentifier win10-x64)
+
+add_executable(DotNetSdk csharponly.cs)
+target_link_libraries(DotNetSdk dotNetSdkLib1)
+set_target_properties(DotNetSdk
+    PROPERTIES
+        VS_GLOBAL_RuntimeIdentifier win10-x64)
diff --git a/Tests/RunCMake/CMP0028/empty.cpp b/Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsSource-result.txt
similarity index 100%
copy from Tests/RunCMake/CMP0028/empty.cpp
copy to Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsSource-result.txt
diff --git a/Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsSource-stderr.txt b/Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsSource-stderr.txt
new file mode 100644
index 0000000..90af627
--- /dev/null
+++ b/Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsSource-stderr.txt
@@ -0,0 +1,7 @@
+CMake Error in CMakeLists.txt:
+  The target "foo" does not currently support add_custom_command as the
+  Visual Studio generators have not yet learned how to generate custom
+  commands in .Net SDK-style projects.
+
+
+CMake Generate step failed.  Build files cannot be regenerated correctly.
diff --git a/Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsSource.cmake b/Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsSource.cmake
new file mode 100644
index 0000000..af18946
--- /dev/null
+++ b/Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsSource.cmake
@@ -0,0 +1,15 @@
+enable_language(CSharp)
+
+if(NOT CMAKE_CSharp_COMPILER)
+    return()
+endif()
+
+set(CMAKE_DOTNET_SDK "Microsoft.NET.Sdk")
+add_custom_command(
+  OUTPUT bar.cs
+  COMMAND copy /A ${CMAKE_CURRENT_SOURCE_DIR}/lib1.cs
+             bar.cs
+  DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/lib1.cs
+  VERBATIM)
+
+add_library(foo SHARED bar.cs)
diff --git a/Tests/RunCMake/CMP0028/empty.cpp b/Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsTarget-result.txt
similarity index 100%
copy from Tests/RunCMake/CMP0028/empty.cpp
copy to Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsTarget-result.txt
diff --git a/Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsTarget-stderr.txt b/Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsTarget-stderr.txt
new file mode 100644
index 0000000..90af627
--- /dev/null
+++ b/Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsTarget-stderr.txt
@@ -0,0 +1,7 @@
+CMake Error in CMakeLists.txt:
+  The target "foo" does not currently support add_custom_command as the
+  Visual Studio generators have not yet learned how to generate custom
+  commands in .Net SDK-style projects.
+
+
+CMake Generate step failed.  Build files cannot be regenerated correctly.
diff --git a/Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsTarget.cmake b/Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsTarget.cmake
new file mode 100644
index 0000000..f5cd317
--- /dev/null
+++ b/Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsTarget.cmake
@@ -0,0 +1,12 @@
+enable_language(CSharp)
+
+if(NOT CMAKE_CSharp_COMPILER)
+    return()
+endif()
+
+set(CMAKE_DOTNET_SDK "Microsoft.NET.Sdk")
+add_library(foo SHARED lib1.cs)
+add_custom_command(TARGET foo
+  PRE_BUILD
+  COMMAND echo "This shouldn't happen!"
+  VERBATIM)
diff --git a/Tests/RunCMake/VsDotnetSdk/csharponly.cs b/Tests/RunCMake/VsDotnetSdk/csharponly.cs
new file mode 100644
index 0000000..f02e8a3
--- /dev/null
+++ b/Tests/RunCMake/VsDotnetSdk/csharponly.cs
@@ -0,0 +1,11 @@
+namespace CSharpOnly
+{
+    class CSharpOnly
+    {
+        public static void Main(string[] args)
+        {
+            int val = Lib1.getResult();
+            return;
+        }
+    }
+}
diff --git a/Tests/RunCMake/VsDotnetSdk/lib1.cs b/Tests/RunCMake/VsDotnetSdk/lib1.cs
new file mode 100644
index 0000000..7a7ae10
--- /dev/null
+++ b/Tests/RunCMake/VsDotnetSdk/lib1.cs
@@ -0,0 +1,10 @@
+namespace CSharpOnly
+{
+    public class Lib1
+    {
+        public static int getResult()
+        {
+            return 23;
+        }
+    }
+}
diff --git a/Tests/RunCMake/list/LIST-nonexistent.cmake b/Tests/RunCMake/list/LIST-nonexistent.cmake
new file mode 100644
index 0000000..ee88548
--- /dev/null
+++ b/Tests/RunCMake/list/LIST-nonexistent.cmake
@@ -0,0 +1,45 @@
+# Various list operations should treat non-existent variables as empty
+# - APPEND
+# - PREPEND
+# - INSERT (only valid index is 0)
+set(nex_l0 "")
+list(APPEND nex_l0 a)
+list(APPEND nex_l0 b)
+if(NOT nex_l0 STREQUAL "a;b")
+  message(FATAL_ERROR "a;b expected, got ${nex_l0}")
+endif()
+
+unset(nex_l1)
+list(APPEND nex_l1 c)
+list(APPEND nex_l1 d)
+if(NOT nex_l1 STREQUAL "c;d")
+  message(FATAL_ERROR "c;d expected, got ${nex_l1}")
+endif()
+
+set(nex_l2 "")
+list(PREPEND nex_l2 E)
+list(PREPEND nex_l2 f)
+if(NOT nex_l2 STREQUAL "f;E")
+  message(FATAL_ERROR "f;E expected, got ${nex_l2}")
+endif()
+
+unset(nex_l3)
+list(PREPEND nex_l3 hi)
+list(PREPEND nex_l3 G)
+if(NOT nex_l3 STREQUAL "G;hi")
+  message(FATAL_ERROR "G;hi expected, got ${nex_l3}")
+endif()
+
+set(nex_l4 "")
+list(INSERT nex_l4 0 j)
+list(INSERT nex_l4 0 kl)
+if(NOT nex_l4 STREQUAL "kl;j")
+  message(FATAL_ERROR "kl;j expected, got ${nex_l4}")
+endif()
+
+unset(nex_l5)
+list(INSERT nex_l5 0 M)
+list(INSERT nex_l5 0 noP)
+if(NOT nex_l5 STREQUAL "noP;M")
+  message(FATAL_ERROR "noP;M expected, got ${nex_l5}")
+endif()
diff --git a/Tests/RunCMake/list/RunCMakeTest.cmake b/Tests/RunCMake/list/RunCMakeTest.cmake
index eb43ee0..adfe255 100644
--- a/Tests/RunCMake/list/RunCMakeTest.cmake
+++ b/Tests/RunCMake/list/RunCMakeTest.cmake
@@ -116,3 +116,6 @@
 # Successful tests
 run_cmake(POP_BACK)
 run_cmake(POP_FRONT)
+
+# Nonexistent variables treated as empty
+run_cmake(LIST-nonexistent)