diff --git a/Help/command/cmake_path.rst b/Help/command/cmake_path.rst index 2a363252d1..e785118b71 100644 --- a/Help/command/cmake_path.rst +++ b/Help/command/cmake_path.rst @@ -456,6 +456,12 @@ meaning of each path component. When the ``NORMALIZE`` option is specified, ```` and ```` are :ref:`normalized ` 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") diff --git a/Help/command/if.rst b/Help/command/if.rst index b7aed1b731..0956a1c11e 100644 --- a/Help/command/if.rst +++ b/Help/command/if.rst @@ -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 ``$`` without their ``NORMALIZE`` option. See :command:`cmake_path(IS_PREFIX)` for more details. diff --git a/Help/manual/cmake-generator-expressions.7.rst b/Help/manual/cmake-generator-expressions.7.rst index 72db5372b4..a396ddbb88 100644 --- a/Help/manual/cmake-generator-expressions.7.rst +++ b/Help/manual/cmake-generator-expressions.7.rst @@ -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 ` 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 diff --git a/Help/manual/cmake-policies.7.rst b/Help/manual/cmake-policies.7.rst index b79d720130..d311e53a75 100644 --- a/Help/manual/cmake-policies.7.rst +++ b/Help/manual/cmake-policies.7.rst @@ -100,6 +100,7 @@ Policies Introduced by CMake 4.5 .. toctree:: :maxdepth: 1 + CMP0223: An empty path is not a prefix of any path. CMP0222: The if() command supports path prefix tests using PATH_IS_PREFIX operator. CMP0221: cmake_host_system_information() DISTRIB_* queries read the host os-release. CMP0220: Languages enabled in subdirectories propagate to the top-level directory. diff --git a/Help/policy/CMP0223.rst b/Help/policy/CMP0223.rst new file mode 100644 index 0000000000..67923e19fa --- /dev/null +++ b/Help/policy/CMP0223.rst @@ -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 ``$`` 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 diff --git a/Help/release/dev/cmake_path-IS_PREFIX-empty.rst b/Help/release/dev/cmake_path-IS_PREFIX-empty.rst new file mode 100644 index 0000000000..dcae37cd2d --- /dev/null +++ b/Help/release/dev/cmake_path-IS_PREFIX-empty.rst @@ -0,0 +1,8 @@ +cmake_path-IS_PREFIX-empty +-------------------------- + +* The :command:`cmake_path(IS_PREFIX)` command and the + :genex:`$` 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`. diff --git a/Source/cmCMakePath.cxx b/Source/cmCMakePath.cxx index d046c93deb..9d68f204ee 100644 --- a/Source/cmCMakePath.cxx +++ b/Source/cmCMakePath.cxx @@ -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(); diff --git a/Source/cmCMakePathCommand.cxx b/Source/cmCMakePathCommand.cxx index c1da62d2b4..219b3bbd20 100644 --- a/Source/cmCMakePathCommand.cxx +++ b/Source/cmCMakePathCommand.cxx @@ -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 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 const& args, cmExecutionStatus& status) { @@ -869,14 +888,16 @@ bool HandleIsPrefixCommand(std::vector 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; diff --git a/Source/cmConditionEvaluator.cxx b/Source/cmConditionEvaluator.cxx index bcd6bebe16..325e3796de 100644 --- a/Source/cmConditionEvaluator.cxx +++ b/Source/cmConditionEvaluator.cxx @@ -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); } diff --git a/Source/cmGeneratorExpressionNode.cxx b/Source/cmGeneratorExpressionNode.cxx index a1ede11a86..7f360709f6 100644 --- a/Source/cmGeneratorExpressionNode.cxx +++ b/Source/cmGeneratorExpressionNode.cxx @@ -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{}; } }, diff --git a/Source/cmPolicies.h b/Source/cmPolicies.h index 6f4f8526bc..80eebc3d59 100644 --- a/Source/cmPolicies.h +++ b/Source/cmPolicies.h @@ -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) \ diff --git a/Tests/RunCMake/CMP0223/CMP0223-NEW.cmake b/Tests/RunCMake/CMP0223/CMP0223-NEW.cmake new file mode 100644 index 0000000000..0396c1c317 --- /dev/null +++ b/Tests/RunCMake/CMP0223/CMP0223-NEW.cmake @@ -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() diff --git a/Tests/RunCMake/CMP0223/CMP0223-OLD.cmake b/Tests/RunCMake/CMP0223/CMP0223-OLD.cmake new file mode 100644 index 0000000000..7991a4e676 --- /dev/null +++ b/Tests/RunCMake/CMP0223/CMP0223-OLD.cmake @@ -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() diff --git a/Tests/RunCMake/CMP0223/CMP0223-WARN-stderr.txt b/Tests/RunCMake/CMP0223/CMP0223-WARN-stderr.txt new file mode 100644 index 0000000000..a2aa21c88d --- /dev/null +++ b/Tests/RunCMake/CMP0223/CMP0223-WARN-stderr.txt @@ -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\. diff --git a/Tests/RunCMake/CMP0223/CMP0223-WARN.cmake b/Tests/RunCMake/CMP0223/CMP0223-WARN.cmake new file mode 100644 index 0000000000..517c2507a8 --- /dev/null +++ b/Tests/RunCMake/CMP0223/CMP0223-WARN.cmake @@ -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() diff --git a/Tests/RunCMake/CMP0223/CMP0223-genex-NEW-check.cmake b/Tests/RunCMake/CMP0223/CMP0223-genex-NEW-check.cmake new file mode 100644 index 0000000000..32eb62fe60 --- /dev/null +++ b/Tests/RunCMake/CMP0223/CMP0223-genex-NEW-check.cmake @@ -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() diff --git a/Tests/RunCMake/CMP0223/CMP0223-genex-NEW.cmake b/Tests/RunCMake/CMP0223/CMP0223-genex-NEW.cmake new file mode 100644 index 0000000000..c6262cfba8 --- /dev/null +++ b/Tests/RunCMake/CMP0223/CMP0223-genex-NEW.cmake @@ -0,0 +1 @@ +include(CMP0223-genex-common.cmake) diff --git a/Tests/RunCMake/CMP0223/CMP0223-genex-OLD-check.cmake b/Tests/RunCMake/CMP0223/CMP0223-genex-OLD-check.cmake new file mode 100644 index 0000000000..f1163f188f --- /dev/null +++ b/Tests/RunCMake/CMP0223/CMP0223-genex-OLD-check.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() diff --git a/Tests/RunCMake/CMP0223/CMP0223-genex-OLD.cmake b/Tests/RunCMake/CMP0223/CMP0223-genex-OLD.cmake new file mode 100644 index 0000000000..c6262cfba8 --- /dev/null +++ b/Tests/RunCMake/CMP0223/CMP0223-genex-OLD.cmake @@ -0,0 +1 @@ +include(CMP0223-genex-common.cmake) diff --git a/Tests/RunCMake/CMP0223/CMP0223-genex-WARN-check.cmake b/Tests/RunCMake/CMP0223/CMP0223-genex-WARN-check.cmake new file mode 100644 index 0000000000..f1163f188f --- /dev/null +++ b/Tests/RunCMake/CMP0223/CMP0223-genex-WARN-check.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() diff --git a/Tests/RunCMake/CMP0223/CMP0223-genex-WARN-stderr.txt b/Tests/RunCMake/CMP0223/CMP0223-genex-WARN-stderr.txt new file mode 100644 index 0000000000..c646a09fd8 --- /dev/null +++ b/Tests/RunCMake/CMP0223/CMP0223-genex-WARN-stderr.txt @@ -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\. diff --git a/Tests/RunCMake/CMP0223/CMP0223-genex-WARN.cmake b/Tests/RunCMake/CMP0223/CMP0223-genex-WARN.cmake new file mode 100644 index 0000000000..c6262cfba8 --- /dev/null +++ b/Tests/RunCMake/CMP0223/CMP0223-genex-WARN.cmake @@ -0,0 +1 @@ +include(CMP0223-genex-common.cmake) diff --git a/Tests/RunCMake/CMP0223/CMP0223-genex-common.cmake b/Tests/RunCMake/CMP0223/CMP0223-genex-common.cmake new file mode 100644 index 0000000000..39f199ea3a --- /dev/null +++ b/Tests/RunCMake/CMP0223/CMP0223-genex-common.cmake @@ -0,0 +1,2 @@ +file(GENERATE OUTPUT "result.txt" CONTENT + "empty=$ normalize=$ nonempty=$") diff --git a/Tests/RunCMake/CMP0223/CMP0223-if-WARN-stderr.txt b/Tests/RunCMake/CMP0223/CMP0223-if-WARN-stderr.txt new file mode 100644 index 0000000000..f2bafa988b --- /dev/null +++ b/Tests/RunCMake/CMP0223/CMP0223-if-WARN-stderr.txt @@ -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\. diff --git a/Tests/RunCMake/CMP0223/CMP0223-if-WARN.cmake b/Tests/RunCMake/CMP0223/CMP0223-if-WARN.cmake new file mode 100644 index 0000000000..142aca247a --- /dev/null +++ b/Tests/RunCMake/CMP0223/CMP0223-if-WARN.cmake @@ -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() diff --git a/Tests/RunCMake/CMP0223/CMakeLists.txt b/Tests/RunCMake/CMP0223/CMakeLists.txt new file mode 100644 index 0000000000..5ff8d3e0ff --- /dev/null +++ b/Tests/RunCMake/CMP0223/CMakeLists.txt @@ -0,0 +1,3 @@ +cmake_minimum_required(VERSION 3.23) +project(${RunCMake_TEST} NONE) +include(${RunCMake_TEST}.cmake) diff --git a/Tests/RunCMake/CMP0223/RunCMakeTest.cmake b/Tests/RunCMake/CMP0223/RunCMakeTest.cmake new file mode 100644 index 0000000000..6e6f964f41 --- /dev/null +++ b/Tests/RunCMake/CMP0223/RunCMakeTest.cmake @@ -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) diff --git a/Tests/RunCMake/CMakeLists.txt b/Tests/RunCMake/CMakeLists.txt index 877679084e..c407722a7b 100644 --- a/Tests/RunCMake/CMakeLists.txt +++ b/Tests/RunCMake/CMakeLists.txt @@ -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}) diff --git a/Tests/RunCMake/cmake_path/IS_PREFIX.cmake b/Tests/RunCMake/cmake_path/IS_PREFIX.cmake index c4c6533105..23e961c163 100644 --- a/Tests/RunCMake/cmake_path/IS_PREFIX.cmake +++ b/Tests/RunCMake/cmake_path/IS_PREFIX.cmake @@ -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) diff --git a/Tests/RunCMake/if/PathIsPrefix.cmake b/Tests/RunCMake/if/PathIsPrefix.cmake index 37f11e6ce4..5268ec5753 100644 --- a/Tests/RunCMake/if/PathIsPrefix.cmake +++ b/Tests/RunCMake/if/PathIsPrefix.cmake @@ -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