mirror of
https://gitlab.kitware.com/cmake/cmake.git
synced 2026-09-25 04:09:36 +03:00
if: Add PATH_IS_PREFIX operator
Testing whether one path is a prefix of another is possible today with
cmake_path(IS_PREFIX), but the idiom needs a separate statement plus a
scratch variable and takes the prefix as a variable name, so projects
reach for if(path MATCHES "^${prefix}") instead. That is wrong whenever
the prefix contains a regex metacharacter, and it accepts siblings,
because '^/a/b' matches '/a/bc'.
Add a binary operator where the left operand is the candidate prefix and
the right is the path, matching the operand order of cmake_path(IS_PREFIX)
and $<PATH:IS_PREFIX> so that the same operation reads the same way on all
three surfaces. Neither operand is normalized, matching the default of
cmake_path(IS_PREFIX) and the existing PATH_EQUAL operator, so '.' and
'..' are compared as ordinary components. The test is non-strict, purely
lexical, and applies no relative-to-absolute reconciliation.
Add policy CMP0222 for compatibility, modeled on CMP0139. The keyword
is consumed as an operator only when it appears in the second argument
slot, so a variable named PATH_IS_PREFIX keeps working in unary and
left-operand position. What the policy covers is such a variable
appearing after another token, as in if(NOT PATH_IS_PREFIX AND other),
which becomes a configure error under NEW.
Since the operator is an if() spelling of cmake_path(IS_PREFIX) and shares
its implementation, test it by asserting the two agree over a corpus of
inputs rather than by restating expected values. Those are pinned by the
cmake_path(IS_PREFIX) test, so the operator's tests stay limited to what
has no command equivalent, and no platform branching is needed because
parity holds whatever the host path model answers.
Fixes: #28040
This commit is contained in:
committed by
Brad King
parent
3d53f7901b
commit
860583ceac
@@ -429,6 +429,40 @@ Path Comparisons
|
||||
|
||||
See :command:`cmake_path(COMPARE)` for more details.
|
||||
|
||||
.. signature:: if(<variable|string> PATH_IS_PREFIX <variable|string>)
|
||||
:target: PATH_IS_PREFIX
|
||||
|
||||
.. versionadded:: 4.5
|
||||
|
||||
True if the path on the left is a prefix of the path on the right.
|
||||
|
||||
.. code-block:: cmake
|
||||
|
||||
# comparison is TRUE
|
||||
if ("/a/b" PATH_IS_PREFIX "/a/b/c")
|
||||
...
|
||||
endif()
|
||||
|
||||
# comparison is FALSE: '/a/bc' is a sibling, not a child
|
||||
if ("/a/b" PATH_IS_PREFIX "/a/bc")
|
||||
...
|
||||
endif()
|
||||
|
||||
Component-wise comparison is superior to a regular expression match
|
||||
against the start of the path. The ``if (<path> MATCHES "^<prefix>")``
|
||||
idiom mishandles regex metacharacters in ``<prefix>`` and accepts
|
||||
siblings.
|
||||
|
||||
The test is lexical, not a containment check. No
|
||||
:ref:`path normalization <Normalization>` is performed, so a ``..``
|
||||
escape still tests true: ``"/a/b" PATH_IS_PREFIX "/a/b/../../etc"``.
|
||||
Normalize with :command:`cmake_path(NORMAL_PATH)` first if that must be
|
||||
rejected.
|
||||
|
||||
Equivalent to :command:`cmake_path(IS_PREFIX)` and
|
||||
``$<PATH:IS_PREFIX>`` without their ``NORMALIZE`` option. See
|
||||
:command:`cmake_path(IS_PREFIX)` for more details.
|
||||
|
||||
Variable Expansion
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ Policies Introduced by CMake 4.5
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
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>
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
CMP0222
|
||||
-------
|
||||
|
||||
.. versionadded:: 4.5
|
||||
|
||||
The :command:`if` command supports path prefix tests using
|
||||
``PATH_IS_PREFIX`` operator.
|
||||
|
||||
The ``OLD`` behavior for this policy is to ignore the ``PATH_IS_PREFIX``
|
||||
operator. The ``NEW`` behavior is to interpret the ``PATH_IS_PREFIX``
|
||||
operator.
|
||||
|
||||
.. |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,6 @@
|
||||
if-PATH_IS_PREFIX
|
||||
-----------------
|
||||
|
||||
* The :command:`if` command gained a ``PATH_IS_PREFIX`` operator to test
|
||||
whether one path is a prefix of another without accessing the filesystem.
|
||||
See policy :policy:`CMP0222`.
|
||||
@@ -66,6 +66,7 @@ auto const keyVERSION_GREATER_EQUAL = "VERSION_GREATER_EQUAL"_s;
|
||||
auto const keyVERSION_LESS = "VERSION_LESS"_s;
|
||||
auto const keyVERSION_LESS_EQUAL = "VERSION_LESS_EQUAL"_s;
|
||||
auto const keyPATH_EQUAL = "PATH_EQUAL"_s;
|
||||
auto const keyPATH_IS_PREFIX = "PATH_IS_PREFIX"_s;
|
||||
|
||||
cmSystemTools::CompareOp const MATCH2CMPOP[5] = {
|
||||
cmSystemTools::OP_LESS, cmSystemTools::OP_LESS_EQUAL,
|
||||
@@ -222,6 +223,7 @@ cmConditionEvaluator::cmConditionEvaluator(cmMakefile& makefile,
|
||||
: Makefile(makefile)
|
||||
, Backtrace(std::move(bt))
|
||||
, Policy139Status(makefile.GetPolicyStatus(cmPolicies::CMP0139))
|
||||
, Policy222Status(makefile.GetPolicyStatus(cmPolicies::CMP0222))
|
||||
{
|
||||
}
|
||||
|
||||
@@ -679,6 +681,26 @@ bool cmConditionEvaluator::HandleLevel2(cmArgumentList& newArgs,
|
||||
"Since the policy is not set the OLD behavior will be used."_s);
|
||||
}
|
||||
}
|
||||
|
||||
else if (this->IsKeyword(keyPATH_IS_PREFIX, *args.next)) {
|
||||
|
||||
if (this->Policy222Status != cmPolicies::OLD &&
|
||||
this->Policy222Status != cmPolicies::WARN) {
|
||||
|
||||
cmValue lhs = this->GetVariableOrString(*args.current);
|
||||
cmValue rhs = this->GetVariableOrString(*args.nextnext);
|
||||
auto const result = cmCMakePath{ *lhs }.IsPrefix(cmCMakePath{ *rhs });
|
||||
newArgs.ReduceTwoArgs(result, args);
|
||||
}
|
||||
|
||||
else if (this->Policy222Status == cmPolicies::WARN) {
|
||||
this->Makefile.IssuePolicyWarning(
|
||||
cmPolicies::CMP0222, {},
|
||||
"PATH_IS_PREFIX will be interpreted as an operator "
|
||||
"when the policy is set to NEW. "
|
||||
"Since the policy is not set the OLD behavior will be used."_s);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -67,4 +67,5 @@ private:
|
||||
cmMakefile& Makefile;
|
||||
cmListFileBacktrace Backtrace;
|
||||
cmPolicies::PolicyStatus Policy139Status;
|
||||
cmPolicies::PolicyStatus Policy222Status;
|
||||
};
|
||||
|
||||
@@ -666,6 +666,10 @@ class cmMakefile;
|
||||
SELECT(POLICY, CMP0221, \
|
||||
"cmake_host_system_information() DISTRIB_* queries read the host " \
|
||||
"os-release.", \
|
||||
4, 5, 0, WARN) \
|
||||
SELECT(POLICY, CMP0222, \
|
||||
"The if() command supports path prefix tests using " \
|
||||
"PATH_IS_PREFIX operator.", \
|
||||
4, 5, 0, WARN)
|
||||
|
||||
#define CM_SELECT_ID(F, A1, A2, A3, A4, A5, A6) F(A1)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,8 @@
|
||||
^CMake Error at CMP0222-NEW-churn\.cmake:[0-9]+ \(if\):
|
||||
if given arguments:
|
||||
|
||||
"NOT" "PATH_IS_PREFIX" "STREQUAL" "yes"
|
||||
|
||||
Unknown arguments specified
|
||||
Call Stack \(most recent call first\):
|
||||
CMakeLists\.txt:[0-9]+ \(include\)$
|
||||
@@ -0,0 +1,8 @@
|
||||
cmake_policy(SET CMP0222 NEW)
|
||||
|
||||
# The compatibility break the policy covers: under NEW the keyword is
|
||||
# consumed as an operator, so a variable of that name is a hard error.
|
||||
set(PATH_IS_PREFIX "yes")
|
||||
if(NOT PATH_IS_PREFIX STREQUAL "yes")
|
||||
message(SEND_ERROR "unreachable")
|
||||
endif()
|
||||
@@ -0,0 +1,13 @@
|
||||
cmake_policy(SET CMP0222 NEW)
|
||||
|
||||
# Under NEW the keyword is interpreted as an operator and evaluated. The
|
||||
# operator's own behavior is covered by the RunCMake.if PathIsPrefix case,
|
||||
# which outlives this directory: the OLD and WARN cases here are removed
|
||||
# once support for the OLD behavior is dropped.
|
||||
if(NOT "/a/b" PATH_IS_PREFIX "/a/b/c")
|
||||
message(SEND_ERROR "if(PATH_IS_PREFIX): '/a/b' not a prefix of '/a/b/c'")
|
||||
endif()
|
||||
|
||||
if("/a/b" PATH_IS_PREFIX "/a/bc")
|
||||
message(SEND_ERROR "if(PATH_IS_PREFIX): '/a/b' wrongly a prefix of '/a/bc'")
|
||||
endif()
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,8 @@
|
||||
^CMake Error at CMP0222-OLD\.cmake:[0-9]+ \(if\):
|
||||
if given arguments:
|
||||
|
||||
"/path1" "PATH_IS_PREFIX" "/path2"
|
||||
|
||||
Unknown arguments specified
|
||||
Call Stack \(most recent call first\):
|
||||
CMakeLists\.txt:[0-9]+ \(include\)$
|
||||
@@ -0,0 +1,13 @@
|
||||
cmake_policy(SET CMP0222 OLD)
|
||||
|
||||
# Still usable as a value under OLD. Must come first: the case below is
|
||||
# fatal.
|
||||
set(PATH_IS_PREFIX "value")
|
||||
if(PATH_IS_PREFIX STREQUAL "value")
|
||||
else()
|
||||
message(SEND_ERROR "PATH_IS_PREFIX not usable as a value under OLD")
|
||||
endif()
|
||||
|
||||
if("/path1" PATH_IS_PREFIX "/path2")
|
||||
message("PATH_IS_PREFIX recognized")
|
||||
endif()
|
||||
@@ -0,0 +1,12 @@
|
||||
CMake Warning \(policy\) at CMP0222-WARN-churn\.cmake:[0-9]+ \(if\):
|
||||
Policy CMP0222 is not set: The if\(\) command supports path prefix tests
|
||||
using PATH_IS_PREFIX operator\. Run "cmake --help-policy CMP0222" for
|
||||
policy details\. Use the cmake_policy command to set the policy and
|
||||
suppress this warning\.
|
||||
|
||||
PATH_IS_PREFIX will be interpreted as an operator when the policy is set to
|
||||
NEW\. Since the policy is not set the OLD behavior will be used\.
|
||||
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,5 @@
|
||||
set(PATH_IS_PREFIX "yes")
|
||||
set(other "yes")
|
||||
if(NOT PATH_IS_PREFIX AND other)
|
||||
message(SEND_ERROR "condition should be false")
|
||||
endif()
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,21 @@
|
||||
CMake Warning \(policy\) at CMP0222-WARN\.cmake:[0-9]+ \(if\):
|
||||
Policy CMP0222 is not set: The if\(\) command supports path prefix tests
|
||||
using PATH_IS_PREFIX operator\. Run "cmake --help-policy CMP0222" for
|
||||
policy details\. Use the cmake_policy command to set the policy and
|
||||
suppress this warning\.
|
||||
|
||||
PATH_IS_PREFIX will be interpreted as an operator when the policy is set to
|
||||
NEW\. Since the policy is not set the OLD behavior will be used\.
|
||||
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\.
|
||||
|
||||
CMake Error at CMP0222-WARN\.cmake:[0-9]+ \(if\):
|
||||
if given arguments:
|
||||
|
||||
"/path1" "PATH_IS_PREFIX" "/path2"
|
||||
|
||||
Unknown arguments specified
|
||||
Call Stack \(most recent call first\):
|
||||
CMakeLists\.txt:[0-9]+ \(include\)
|
||||
@@ -0,0 +1,3 @@
|
||||
if("/path1" PATH_IS_PREFIX "/path2")
|
||||
message("PATH_IS_PREFIX recognized")
|
||||
endif()
|
||||
@@ -0,0 +1,3 @@
|
||||
cmake_minimum_required(VERSION 3.23)
|
||||
project(${RunCMake_TEST} NONE)
|
||||
include(${RunCMake_TEST}.cmake)
|
||||
@@ -0,0 +1,7 @@
|
||||
include(RunCMake)
|
||||
|
||||
run_cmake(CMP0222-OLD)
|
||||
run_cmake(CMP0222-WARN)
|
||||
run_cmake(CMP0222-WARN-churn)
|
||||
run_cmake(CMP0222-NEW)
|
||||
run_cmake(CMP0222-NEW-churn)
|
||||
@@ -186,6 +186,7 @@ if(WIN32)
|
||||
endif()
|
||||
add_RunCMake_test(CMP0217)
|
||||
add_RunCMake_test(CMP0219)
|
||||
add_RunCMake_test(CMP0222)
|
||||
|
||||
if(CMAKE_C_COMPILER_ID STREQUAL "MSVC")
|
||||
add_RunCMake_test(CMP0194 -DCMAKE_C_COMPILER_VERSION=${CMAKE_C_COMPILER_VERSION})
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
cmake_policy(SET CMP0222 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
|
||||
# each input should evaluate to is pinned by the cmake_path(IS_PREFIX) test;
|
||||
# this asserts only that the two surfaces cannot drift apart. Neither
|
||||
# normalizes, so the comparison is against the plain form of the command.
|
||||
#
|
||||
# Because nothing below states an expected value, the host path model needs
|
||||
# no branching: parity has to hold on every platform whatever the answer is.
|
||||
function(assert_parity prefix path)
|
||||
set(prefix_var "${prefix}")
|
||||
cmake_path(IS_PREFIX prefix_var "${path}" expected)
|
||||
if(expected)
|
||||
set(expected TRUE)
|
||||
else()
|
||||
set(expected FALSE)
|
||||
endif()
|
||||
if(prefix PATH_IS_PREFIX path)
|
||||
set(actual TRUE)
|
||||
else()
|
||||
set(actual FALSE)
|
||||
endif()
|
||||
if(NOT actual STREQUAL expected)
|
||||
message(SEND_ERROR
|
||||
"if('${prefix}' PATH_IS_PREFIX '${path}') is ${actual}, but "
|
||||
"cmake_path(IS_PREFIX) says ${expected}")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# Each pair appears in both roles, so a reversed call cannot pass.
|
||||
assert_parity("/a/b" "/a/b/c")
|
||||
assert_parity("/a/b/c" "/a/b")
|
||||
assert_parity("/a/b" "/a/b")
|
||||
|
||||
# Trailing separators, which are significant and asymmetric.
|
||||
assert_parity("/a/b" "/a/b/")
|
||||
assert_parity("/a/b/" "/a/b")
|
||||
assert_parity("/a/b/" "/a/b/c")
|
||||
|
||||
# Without NORMALIZE, '.' and '..' are ordinary components.
|
||||
assert_parity("/a/b" "/a/b/../../etc")
|
||||
assert_parity("/a/b" "/a/./b//c")
|
||||
assert_parity("." "./a/b")
|
||||
assert_parity("." "a/b")
|
||||
|
||||
# Duplicate separators are not components.
|
||||
assert_parity("a/b" "a///b")
|
||||
|
||||
# Sibling sharing a textual prefix.
|
||||
assert_parity("/a/b" "/a/bc")
|
||||
|
||||
# Relative and absolute paths are not reconciled.
|
||||
assert_parity("/a" "b/c")
|
||||
assert_parity("b" "b/c")
|
||||
assert_parity("/" "b/c")
|
||||
assert_parity("/" "/a/b")
|
||||
|
||||
# Empty operands in each position.
|
||||
assert_parity("" "/a/b")
|
||||
assert_parity("/a" "")
|
||||
assert_parity("" "")
|
||||
|
||||
# Host path model: backslashes, case, drive-relative paths, UNC root-names.
|
||||
assert_parity("C:\\a" "C:\\a\\b")
|
||||
assert_parity("c:/a" "C:/A/b")
|
||||
assert_parity("C:/" "C:foo")
|
||||
assert_parity("//host/share" "//host/share/a")
|
||||
assert_parity("/a" "//a/b")
|
||||
|
||||
# Literal operands, not just variables.
|
||||
if(NOT "/a/b" PATH_IS_PREFIX "/a/b/c")
|
||||
message(SEND_ERROR "if(PATH_IS_PREFIX): literal operands rejected")
|
||||
endif()
|
||||
|
||||
# NOT composes with the operator, which is the motivating guard idiom.
|
||||
set(guard_ok FALSE)
|
||||
if(NOT "/a/b" PATH_IS_PREFIX "/a/bc")
|
||||
set(guard_ok TRUE)
|
||||
endif()
|
||||
if(NOT guard_ok)
|
||||
message(SEND_ERROR "if(NOT <prefix> PATH_IS_PREFIX <path>) did not compose")
|
||||
endif()
|
||||
|
||||
# Works in an elseif() chain, which a helper function could not.
|
||||
if("/a" PATH_IS_PREFIX "/x/y")
|
||||
message(SEND_ERROR "if(PATH_IS_PREFIX): '/a' matched '/x/y'")
|
||||
elseif("/a/b" PATH_IS_PREFIX "/a/b/c")
|
||||
else()
|
||||
message(SEND_ERROR "elseif(<prefix> PATH_IS_PREFIX <path>) did not match")
|
||||
endif()
|
||||
|
||||
# The motivating case: a prefix that if(MATCHES) cannot express.
|
||||
set(prefix "/proj/lib+ssl(v2)/inc.d")
|
||||
if(NOT "${prefix}" PATH_IS_PREFIX "${prefix}/f.h")
|
||||
message(SEND_ERROR "if(PATH_IS_PREFIX): regex metacharacters in prefix")
|
||||
endif()
|
||||
|
||||
# The keyword is consumed as an operator only with both a preceding and a
|
||||
# following token, so it stays usable as a variable otherwise. Neither
|
||||
# condition may be written with a leading NOT: that would put the keyword in
|
||||
# the operator slot and make these a hard error.
|
||||
set(PATH_IS_PREFIX "yes")
|
||||
if(PATH_IS_PREFIX)
|
||||
else()
|
||||
message(SEND_ERROR "if(PATH_IS_PREFIX): unary use broken")
|
||||
endif()
|
||||
if(PATH_IS_PREFIX STREQUAL "yes")
|
||||
else()
|
||||
message(SEND_ERROR "if(PATH_IS_PREFIX STREQUAL ...): left-operand use broken")
|
||||
endif()
|
||||
@@ -13,6 +13,7 @@ if(NOT MSYS)
|
||||
endif()
|
||||
run_cmake(IsDirectory)
|
||||
run_cmake(IsDirectoryLong)
|
||||
run_cmake(PathIsPrefix)
|
||||
run_cmake(duplicate-deep-else)
|
||||
run_cmake(duplicate-else)
|
||||
run_cmake(duplicate-else-after-elseif)
|
||||
|
||||
Reference in New Issue
Block a user