mirror of
https://gitlab.kitware.com/cmake/cmake.git
synced 2026-09-29 04:09:35 +03:00
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
113 lines
3.6 KiB
CMake
113 lines
3.6 KiB
CMake
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
|
|
# 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()
|