cmake_path: An empty path is not a prefix of any path

cmake_path(IS_PREFIX) and $<PATH:IS_PREFIX> treated an empty path as a
prefix of every path, including another empty path, following
std::filesystem::path.  A prefix that is empty because a variable was
set to an empty value, or because a generator expression argument
expanded to nothing, therefore satisfied a check meant to reject it.

Return false for an empty prefix, in cmCMakePath::IsPrefix so that every
caller shares one implementation and both the plain and NORMALIZE forms
are covered.  Normalizing an empty path leaves it empty, so no separate
handling of NORMALIZE is needed.  Unlike the component comparison, which
follows std::filesystem::path deliberately, IsPrefix has no counterpart
in the standard: it borrows path iteration but the predicate itself is
defined by CMake, so an empty prefix is a gap to fill rather than a
standard answer to override.

Add policy CMP0223 and restore the old result behind it at the two
released surfaces.  The other callers of IsPrefix, source_group() and
the Makefile generator's source classification, take the new behavior
ungated: source_group() rejects an empty TREE argument before reaching
it, and the generator passes the source and binary directories.

The if(PATH_IS_PREFIX) operator, new in this same release, follows the
policy too rather than simply taking the new behavior, so that it agrees
with cmake_path(IS_PREFIX) in every policy state and the parity
assertions in its test hold unconditionally.

Fixes: #28077
This commit is contained in:
Mickaël Germain
2026-09-17 10:05:27 -04:00
committed by Brad King
parent 34bbc91e3b
commit 7192a2b798
30 changed files with 243 additions and 16 deletions
+6
View File
@@ -456,6 +456,12 @@ meaning of each path component.
When the ``NORMALIZE`` option is specified, ``<path-var>`` and ``<input>``
are :ref:`normalized <Normalization>` before the check.
An empty path is not a prefix of any path.
.. versionchanged:: 4.5
An empty path was previously a prefix of every path. See policy
:policy:`CMP0223`.
.. code-block:: cmake
set(path "/a/b/c")
+3
View File
@@ -459,6 +459,9 @@ Path Comparisons
Normalize with :command:`cmake_path(NORMAL_PATH)` first if that must be
rejected.
An empty prefix is not a prefix of any path. See policy
:policy:`CMP0223`.
Equivalent to :command:`cmake_path(IS_PREFIX)` and
``$<PATH:IS_PREFIX>`` without their ``NORMALIZE`` option. See
:command:`cmake_path(IS_PREFIX)` for more details.
@@ -1236,6 +1236,12 @@ All paths are expected to be in cmake-style format.
When the ``NORMALIZE`` option is specified, ``path`` and ``input`` are
:ref:`normalized <Normalization>` before the check.
An empty ``path`` is not a prefix of any ``input``.
.. versionchanged:: 4.5
An empty ``path`` was previously a prefix of every ``input``. See
policy :policy:`CMP0223`.
.. _GenEx Path Decomposition:
Path Decomposition
+1
View File
@@ -100,6 +100,7 @@ Policies Introduced by CMake 4.5
.. toctree::
:maxdepth: 1
CMP0223: An empty path is not a prefix of any path. </policy/CMP0223>
CMP0222: The if() command supports path prefix tests using PATH_IS_PREFIX operator. </policy/CMP0222>
CMP0221: cmake_host_system_information() DISTRIB_* queries read the host os-release. </policy/CMP0221>
CMP0220: Languages enabled in subdirectories propagate to the top-level directory. </policy/CMP0220>
+27
View File
@@ -0,0 +1,27 @@
CMP0223
-------
.. versionadded:: 4.5
An empty path is not a prefix of any path.
:command:`cmake_path(IS_PREFIX)` and the ``$<PATH:IS_PREFIX>`` generator
expression treat an empty path as a prefix of every path, including
another empty path. A prefix that is empty because a variable was set to
an empty value, or because a generator expression argument expanded to
nothing, therefore satisfies a check that was meant to reject it.
The :command:`if` command's ``PATH_IS_PREFIX`` operator is new in the same
release and has no previous behavior of its own, but it follows this
policy so that it agrees with :command:`cmake_path(IS_PREFIX)` in every
policy state.
The ``OLD`` behavior for this policy is to treat an empty path as a prefix
of every path. The ``NEW`` behavior is to treat an empty path as a prefix
of no path.
.. |INTRODUCED_IN_CMAKE_VERSION| replace:: 4.5
.. |WARNS_OR_DOES_NOT_WARN| replace:: warns
.. include:: include/STANDARD_ADVICE.rst
.. include:: include/DEPRECATED.rst
@@ -0,0 +1,8 @@
cmake_path-IS_PREFIX-empty
--------------------------
* The :command:`cmake_path(IS_PREFIX)` command and the
:genex:`$<PATH:IS_PREFIX>` generator expression no longer treat
an empty path as a prefix of every path. The :command:`if` command's
``PATH_IS_PREFIX`` operator follows the same rule.
See policy :policy:`CMP0223`.
+5
View File
@@ -83,6 +83,11 @@ cmCMakePath cmCMakePath::Absolute(cm::filesystem::path const& base) const
bool cmCMakePath::IsPrefix(cmCMakePath const& path) const
{
// An empty path is not a prefix of any path, including another empty path.
if (this->Path.empty()) {
return false;
}
auto prefix_it = this->Path.begin();
auto prefix_end = this->Path.end();
auto path_it = path.Path.begin();
+26 -5
View File
@@ -20,6 +20,7 @@
#include "cmExecutionStatus.h"
#include "cmList.h"
#include "cmMakefile.h"
#include "cmPolicies.h"
#include "cmRange.h"
#include "cmStringAlgorithms.h"
#include "cmSubcommandTable.h"
@@ -839,6 +840,24 @@ bool HandleIsRelativeCommand(std::vector<std::string> const& args,
return true;
}
// CMP0223: an empty path used to be a prefix of every path.
bool IsPrefixCMP0223(cmCMakePath const& prefix, cmMakefile& mf)
{
if (!prefix.IsEmpty()) {
return false;
}
switch (mf.GetPolicyStatus(cmPolicies::CMP0223)) {
case cmPolicies::WARN:
mf.IssuePolicyWarning(cmPolicies::CMP0223);
CM_FALLTHROUGH;
case cmPolicies::OLD:
return true;
case cmPolicies::NEW:
break;
}
return false;
}
bool HandleIsPrefixCommand(std::vector<std::string> const& args,
cmExecutionStatus& status)
{
@@ -869,14 +888,16 @@ bool HandleIsPrefixCommand(std::vector<std::string> const& args,
return false;
}
bool isPrefix;
cmCMakePath prefix{ inputPath };
cmCMakePath value{ input };
if (arguments.Normalize) {
isPrefix =
cmCMakePath(inputPath).Normal().IsPrefix(cmCMakePath(input).Normal());
} else {
isPrefix = cmCMakePath(inputPath).IsPrefix(input);
prefix = prefix.Normal();
value = value.Normal();
}
bool const isPrefix =
prefix.IsPrefix(value) || IsPrefixCMP0223(prefix, status.GetMakefile());
status.GetMakefile().AddDefinitionBool(output, isPrefix);
return true;
+21 -1
View File
@@ -117,6 +117,24 @@ bool looksLikeSpecialVariable(std::string const& var,
return ((prefix.size() + 3) <= varNameLen) &&
cmHasPrefix(var, cmStrCat(prefix, '{')) && var[varNameLen - 1] == '}';
}
// CMP0223: an empty path used to be a prefix of every path.
bool IsPrefixCMP0223(cmCMakePath const& prefix, cmMakefile& mf)
{
if (!prefix.IsEmpty()) {
return false;
}
switch (mf.GetPolicyStatus(cmPolicies::CMP0223)) {
case cmPolicies::WARN:
mf.IssuePolicyWarning(cmPolicies::CMP0223);
CM_FALLTHROUGH;
case cmPolicies::OLD:
return true;
case cmPolicies::NEW:
break;
}
return false;
}
} // anonymous namespace
#if defined(__SUNPRO_CC)
@@ -689,7 +707,9 @@ bool cmConditionEvaluator::HandleLevel2(cmArgumentList& newArgs,
cmValue lhs = this->GetVariableOrString(*args.current);
cmValue rhs = this->GetVariableOrString(*args.nextnext);
auto const result = cmCMakePath{ *lhs }.IsPrefix(cmCMakePath{ *rhs });
cmCMakePath const prefix{ *lhs };
auto const result = prefix.IsPrefix(cmCMakePath{ *rhs }) ||
IsPrefixCMP0223(prefix, this->Makefile);
newArgs.ReduceTwoArgs(result, args);
}
+25 -4
View File
@@ -964,6 +964,25 @@ bool GetNumericArguments(
return true;
}
// CMP0223: an empty path used to be a prefix of every path.
bool IsPrefixCMP0223(cmCMakePath const& prefix, cm::GenEx::Evaluation* eval)
{
if (!prefix.IsEmpty()) {
return false;
}
cmLocalGenerator const* const lg = eval->Context.LG;
switch (lg->GetPolicyStatus(cmPolicies::CMP0223)) {
case cmPolicies::WARN:
lg->IssuePolicyWarning(cmPolicies::CMP0223, {}, {}, eval->Backtrace);
CM_FALLTHROUGH;
case cmPolicies::OLD:
return true;
case cmPolicies::NEW:
break;
}
return false;
}
bool CheckPathParametersEx(cm::GenEx::Evaluation* eval,
GeneratorExpressionContent const* cnt,
cm::string_view option, std::size_t count,
@@ -1208,12 +1227,14 @@ static const struct PathNode : public cmGeneratorExpressionNode
if (CheckPathParametersEx(
ev, cnt, normalize ? "IS_PREFIX,NORMALIZE"_s : "IS_PREFIX"_s,
args.size(), 2)) {
cmCMakePath prefix{ args[0] };
cmCMakePath value{ args[1] };
if (normalize) {
return ToString(cmCMakePath{ args[0] }.Normal().IsPrefix(
cmCMakePath{ args[1] }.Normal()));
prefix = prefix.Normal();
value = value.Normal();
}
return ToString(
cmCMakePath{ args[0] }.IsPrefix(cmCMakePath{ args[1] }));
return ToString(prefix.IsPrefix(value) ||
IsPrefixCMP0223(prefix, ev));
}
return std::string{};
} },
+3 -1
View File
@@ -670,7 +670,9 @@ class cmMakefile;
SELECT(POLICY, CMP0222, \
"The if() command supports path prefix tests using " \
"PATH_IS_PREFIX operator.", \
4, 5, 0, WARN)
4, 5, 0, WARN) \
SELECT(POLICY, CMP0223, "An empty path is not a prefix of any path.", 4, 5, \
0, WARN)
#define CM_SELECT_ID(F, A1, A2, A3, A4, A5, A6) F(A1)
#define CM_FOR_EACH_POLICY_ID(POLICY) \
+26
View File
@@ -0,0 +1,26 @@
cmake_policy(SET CMP0223 NEW)
set(prefix "")
cmake_path(IS_PREFIX prefix "/a/b" output)
if(output)
message(SEND_ERROR "empty prefix is a prefix of '/a/b' under NEW")
endif()
cmake_path(IS_PREFIX prefix "" output)
if(output)
message(SEND_ERROR "empty prefix is a prefix of the empty path under NEW")
endif()
# NORMALIZE takes the same path, because normalizing an empty path leaves
# it empty.
cmake_path(IS_PREFIX prefix "/a/b" NORMALIZE output)
if(output)
message(SEND_ERROR "empty prefix is a prefix of '/a/b' under NEW, NORMALIZE")
endif()
# A non-empty prefix is unaffected.
set(prefix "/a")
cmake_path(IS_PREFIX prefix "/a/b" output)
if(NOT output)
message(SEND_ERROR "'/a' is not a prefix of '/a/b' under NEW")
endif()
+12
View File
@@ -0,0 +1,12 @@
cmake_policy(SET CMP0223 OLD)
set(prefix "")
cmake_path(IS_PREFIX prefix "/a/b" output)
if(NOT output)
message(SEND_ERROR "empty prefix is not a prefix of '/a/b' under OLD")
endif()
cmake_path(IS_PREFIX prefix "" output)
if(NOT output)
message(SEND_ERROR "empty prefix is not a prefix of the empty path under OLD")
endif()
@@ -0,0 +1,8 @@
CMake Warning \(policy\) at CMP0223-WARN\.cmake:[0-9]+ \(cmake_path\):
Policy CMP0223 is not set: An empty path is not a prefix of any path\. Run
"cmake --help-policy CMP0223" for policy details\. Use the cmake_policy
command to set the policy and suppress this warning\.
Call Stack \(most recent call first\):
CMakeLists\.txt:[0-9]+ \(include\)
This warning is for project developers\. Use -Wno-author or -Wno-policy to
suppress it\.
@@ -0,0 +1,6 @@
# Policy deliberately not set, so the warning fires and OLD behavior applies.
set(prefix "")
cmake_path(IS_PREFIX prefix "/a/b" output)
if(NOT output)
message(SEND_ERROR "empty prefix is not a prefix of '/a/b' under WARN")
endif()
@@ -0,0 +1,5 @@
file(READ "${RunCMake_TEST_BINARY_DIR}/result.txt" generated)
set(expected "empty=0 normalize=0 nonempty=1")
if(NOT generated STREQUAL expected)
set(RunCMake_TEST_FAILED "generated: ${generated}\nexpected: ${expected}")
endif()
@@ -0,0 +1 @@
include(CMP0223-genex-common.cmake)
@@ -0,0 +1,5 @@
file(READ "${RunCMake_TEST_BINARY_DIR}/result.txt" generated)
set(expected "empty=1 normalize=1 nonempty=1")
if(NOT generated STREQUAL expected)
set(RunCMake_TEST_FAILED "generated: ${generated}\nexpected: ${expected}")
endif()
@@ -0,0 +1 @@
include(CMP0223-genex-common.cmake)
@@ -0,0 +1,5 @@
file(READ "${RunCMake_TEST_BINARY_DIR}/result.txt" generated)
set(expected "empty=1 normalize=1 nonempty=1")
if(NOT generated STREQUAL expected)
set(RunCMake_TEST_FAILED "generated: ${generated}\nexpected: ${expected}")
endif()
@@ -0,0 +1,4 @@
CMake Warning \(policy\) at CMP0223-genex-common\.cmake:[0-9]+ \(file\):
Policy CMP0223 is not set: An empty path is not a prefix of any path\. Run
"cmake --help-policy CMP0223" for policy details\. Use the cmake_policy
command to set the policy and suppress this warning\.
@@ -0,0 +1 @@
include(CMP0223-genex-common.cmake)
@@ -0,0 +1,2 @@
file(GENERATE OUTPUT "result.txt" CONTENT
"empty=$<PATH:IS_PREFIX,,/a/b> normalize=$<PATH:IS_PREFIX,NORMALIZE,,/a/b> nonempty=$<PATH:IS_PREFIX,/a,/a/b>")
@@ -0,0 +1,8 @@
CMake Warning \(policy\) at CMP0223-if-WARN\.cmake:[0-9]+ \(if\):
Policy CMP0223 is not set: An empty path is not a prefix of any path\. Run
"cmake --help-policy CMP0223" for policy details\. Use the cmake_policy
command to set the policy and suppress this warning\.
Call Stack \(most recent call first\):
CMakeLists\.txt:[0-9]+ \(include\)
This warning is for project developers\. Use -Wno-author or -Wno-policy to
suppress it\.
@@ -0,0 +1,6 @@
cmake_policy(SET CMP0222 NEW)
# CMP0223 deliberately not set, so the warning fires and OLD behavior applies.
if(NOT "" PATH_IS_PREFIX "/a/b")
message(SEND_ERROR "empty prefix is not a prefix of '/a/b' under WARN")
endif()
+3
View File
@@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.23)
project(${RunCMake_TEST} NONE)
include(${RunCMake_TEST}.cmake)
+11
View File
@@ -0,0 +1,11 @@
include(RunCMake)
run_cmake(CMP0223-OLD)
run_cmake(CMP0223-WARN)
run_cmake(CMP0223-NEW)
run_cmake_with_options(CMP0223-genex-OLD -DCMAKE_POLICY_DEFAULT_CMP0223=OLD)
run_cmake_with_options(CMP0223-genex-NEW -DCMAKE_POLICY_DEFAULT_CMP0223=NEW)
run_cmake(CMP0223-genex-WARN)
run_cmake(CMP0223-if-WARN)
+1
View File
@@ -187,6 +187,7 @@ endif()
add_RunCMake_test(CMP0217)
add_RunCMake_test(CMP0219)
add_RunCMake_test(CMP0222)
add_RunCMake_test(CMP0223)
if(CMAKE_C_COMPILER_ID STREQUAL "MSVC")
add_RunCMake_test(CMP0194 -DCMAKE_C_COMPILER_VERSION=${CMAKE_C_COMPILER_VERSION})
+6 -5
View File
@@ -1,3 +1,4 @@
cmake_policy(SET CMP0223 NEW)
include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
unset (errors)
@@ -99,15 +100,15 @@ if (NOT output)
list (APPEND errors "'${prefix}' is not prefix of './a/b'")
endif()
# The empty path is a prefix of every path, including itself.
# The empty path is not a prefix of any path, including itself.
set (prefix "")
cmake_path(IS_PREFIX prefix "/a/b" output)
if (NOT output)
list (APPEND errors "the empty path is not prefix of '/a/b'")
if (output)
list (APPEND errors "the empty path is a prefix of '/a/b'")
endif()
cmake_path(IS_PREFIX prefix "" output)
if (NOT output)
list (APPEND errors "the empty path is not prefix of itself")
if (output)
list (APPEND errors "the empty path is a prefix of itself")
endif()
set (prefix "/a")
cmake_path(IS_PREFIX prefix "" output)
+1
View File
@@ -1,4 +1,5 @@
cmake_policy(SET CMP0222 NEW)
cmake_policy(SET CMP0223 NEW)
# The operator is an if() spelling of cmake_path(IS_PREFIX), so assert that
# the two agree rather than repeating a table of expected values here. What