Browse Source

project: Always set <PROJECT-NAME>_* as normal variables

Re-introduce the behavior originally introduced in CMake 3.30.3 by
commit c1ece78d11 (project: non cache <project> prefix variables are
also created, 2024-08-27, v3.30.3~2^2), but this time with a policy for
compatibility.

Issue: #25714
Issue: #26243
Craig Scott 1 year ago
parent
commit
e5a9ccbcc8

+ 11 - 20
Help/command/project.rst

@@ -44,27 +44,18 @@ Projects should not rely on ``<PROJECT-NAME>_SOURCE_DIR`` or
 ``<PROJECT-NAME>_BINARY_DIR`` holding a particular value outside of the scope
 of the call to ``project()`` or one of its child scopes.
 
-.. versionchanged:: 3.30.3
+.. versionchanged:: 3.30
   ``<PROJECT-NAME>_SOURCE_DIR``, ``<PROJECT-NAME>_BINARY_DIR``, and
-  ``<PROJECT-NAME>_IS_TOP_LEVEL`` are always set as non-cache variables by
-  ``project(<PROJECT-NAME> ...)``.
-
-.. versionchanged:: 3.30.4
-  The variables ``<PROJECT-NAME>_SOURCE_DIR``, ``<PROJECT-NAME>_BINARY_DIR``,
-  and ``<PROJECT-NAME>_IS_TOP_LEVEL`` are only set as non-cache variables if
-  they are already set as cache or non-cache variables when
-  ``project(<PROJECT-NAME> ...)`` is called.
-  Note that this logic is flawed, as it can result in different behavior
-  between the first and subsequent runs because cache variables won't exist
-  on the first run, but they will on subsequent runs.
-
-.. versionchanged:: 3.30.5
-  The variables ``<PROJECT-NAME>_SOURCE_DIR``, ``<PROJECT-NAME>_BINARY_DIR``,
-  and ``<PROJECT-NAME>_IS_TOP_LEVEL`` are only set as non-cache variables if
-  they are already set as non-cache variables when
-  ``project(<PROJECT-NAME> ...)`` is called.
-  Unlike the flawed behavior of 3.30.4, non-cache variables will not be set
-  if only cache variables of the same name are set.
+  ``<PROJECT-NAME>_IS_TOP_LEVEL``, if already set as normal variables when
+  ``project(<PROJECT-NAME> ...)`` is called, are updated by the call.
+  Cache entries by the same names are always set as before.
+  See release notes for 3.30.3, 3.30.4, and 3.30.5 for details.
+
+.. versionchanged:: 3.31
+  ``<PROJECT-NAME>_SOURCE_DIR``, ``<PROJECT-NAME>_BINARY_DIR``, and
+  ``<PROJECT-NAME>_IS_TOP_LEVEL`` are always set as normal variables by
+  ``project(<PROJECT-NAME> ...)``.  See policy :policy:`CMP0180`.
+  Cache entries by the same names are always set as before.
 
 Options
 ^^^^^^^

+ 1 - 0
Help/manual/cmake-policies.7.rst

@@ -57,6 +57,7 @@ Policies Introduced by CMake 3.31
 .. toctree::
    :maxdepth: 1
 
+   CMP0180: project() always sets <PROJECT-NAME>_* as normal variables. </policy/CMP0180>
    CMP0179: De-duplication of static libraries on link lines keeps first occurrence. </policy/CMP0179>
    CMP0178: Test command lines preserve empty arguments. </policy/CMP0178>
    CMP0177: install() DESTINATION paths are normalized. </policy/CMP0177>

+ 36 - 0
Help/policy/CMP0180.rst

@@ -0,0 +1,36 @@
+CMP0180
+-------
+
+.. versionadded:: 3.31
+
+:command:`project` always sets ``<PROJECT-NAME>_*`` as normal variables.
+
+In CMake 3.29 and below, the :command:`project` command set
+:variable:`<PROJECT-NAME>_SOURCE_DIR`, :variable:`<PROJECT-NAME>_BINARY_DIR`,
+and :variable:`<PROJECT-NAME>_IS_TOP_LEVEL` as cache entries, but not as
+normal variables.  CMake 3.30 started setting them as normal variables,
+but only if they are already set as normal variables.  This was needed to
+preserve support for some :module:`FetchContent` use cases under policy
+:policy:`CMP0169`'s NEW behavior, while also preserving behavior of nested
+directories that call :command:`project` with the same project name.
+See release notes for 3.30.3, 3.30.4, and 3.30.5 for details.
+
+CMake 3.31 and later prefer to always set ``<PROJECT-NAME>_SOURCE_DIR``,
+``<PROJECT-NAME>_BINARY_DIR``, and ``<PROJECT-NAME>_IS_TOP_LEVEL``, as both
+cache entries and normal variables, regardless of what cache or normal
+variables already exist.  This policy provides compatibility for projects
+that have not been updated to expect this behavior.
+
+The ``OLD`` behavior for this policy will only set normal variables for
+``<PROJECT-NAME>_SOURCE_DIR``, ``<PROJECT-NAME>_BINARY_DIR``, and
+``<PROJECT-NAME>_IS_TOP_LEVEL`` if there is already a normal variable by that
+name when :command:`project` is called.
+The ``NEW`` behavior for this policy will always set normal variables for
+``<PROJECT-NAME>_SOURCE_DIR``, ``<PROJECT-NAME>_BINARY_DIR``, and
+``<PROJECT-NAME>_IS_TOP_LEVEL`` when :command:`project` is called.
+
+.. |INTRODUCED_IN_CMAKE_VERSION| replace:: 3.31
+.. |WARNS_OR_DOES_NOT_WARN| replace:: does *not* warn
+.. include:: STANDARD_ADVICE.txt
+
+.. include:: DEPRECATED.txt

+ 7 - 0
Help/release/dev/project-vars-policy.rst

@@ -0,0 +1,7 @@
+project-vars-policy
+-------------------
+
+* The :command:`project` command now always sets
+  :variable:`<PROJECT-NAME>_SOURCE_DIR`, :variable:`<PROJECT-NAME>_BINARY_DIR`,
+  and :variable:`<PROJECT-NAME>_IS_TOP_LEVEL` as both normal variables and
+  cache entries.  See policy :policy:`CMP0180`.

+ 4 - 1
Source/cmPolicies.h

@@ -549,7 +549,10 @@ class cmMakefile;
   SELECT(POLICY, CMP0179,                                                     \
          "De-duplication of static libraries on link lines keeps first "      \
          "occurrence.",                                                       \
-         3, 31, 0, cmPolicies::WARN)
+         3, 31, 0, cmPolicies::WARN)                                          \
+  SELECT(POLICY, CMP0180,                                                     \
+         "project() always sets <PROJECT-NAME>_* as normal variables.", 3,    \
+         31, 0, cmPolicies::WARN)
 
 #define CM_SELECT_ID(F, A1, A2, A3, A4, A5, A6) F(A1)
 #define CM_FOR_EACH_POLICY_ID(POLICY)                                         \

+ 5 - 3
Source/cmProjectCommand.cxx

@@ -58,11 +58,13 @@ bool cmProjectCommand(std::vector<std::string> const& args,
 
   mf.SetProjectName(projectName);
 
+  cmPolicies::PolicyStatus cmp0180 = mf.GetPolicyStatus(cmPolicies::CMP0180);
+
   std::string varName = cmStrCat(projectName, "_BINARY_DIR"_s);
   bool nonCacheVarAlreadySet = mf.IsNormalDefinitionSet(varName);
   mf.AddCacheDefinition(varName, mf.GetCurrentBinaryDirectory(),
                         "Value Computed by CMake", cmStateEnums::STATIC);
-  if (nonCacheVarAlreadySet) {
+  if (cmp0180 == cmPolicies::NEW || nonCacheVarAlreadySet) {
     mf.AddDefinition(varName, mf.GetCurrentBinaryDirectory());
   }
 
@@ -70,7 +72,7 @@ bool cmProjectCommand(std::vector<std::string> const& args,
   nonCacheVarAlreadySet = mf.IsNormalDefinitionSet(varName);
   mf.AddCacheDefinition(varName, mf.GetCurrentSourceDirectory(),
                         "Value Computed by CMake", cmStateEnums::STATIC);
-  if (nonCacheVarAlreadySet) {
+  if (cmp0180 == cmPolicies::NEW || nonCacheVarAlreadySet) {
     mf.AddDefinition(varName, mf.GetCurrentSourceDirectory());
   }
 
@@ -85,7 +87,7 @@ bool cmProjectCommand(std::vector<std::string> const& args,
   nonCacheVarAlreadySet = mf.IsNormalDefinitionSet(varName);
   mf.AddCacheDefinition(varName, mf.IsRootMakefile() ? "ON" : "OFF",
                         "Value Computed by CMake", cmStateEnums::STATIC);
-  if (nonCacheVarAlreadySet) {
+  if (cmp0180 == cmPolicies::NEW || nonCacheVarAlreadySet) {
     mf.AddDefinition(varName, mf.IsRootMakefile() ? "ON" : "OFF");
   }
 

+ 16 - 0
Tests/RunCMake/project/CMP0180-NEW-stdout.txt

@@ -0,0 +1,16 @@
+(-- )?From subdir1:
+  CMP0180-NEW_SOURCE_DIR = [^
+]+/project/subdir1
+  CMP0180-NEW_BINARY_DIR = [^
+]+/project/CMP0180-NEW-build/subdir1
+  CMP0180-NEW_IS_TOP_LEVEL = OFF
+(-- )?From subdir2:
+  CMP0180-NEW_SOURCE_DIR = [^
+]+/project
+  CMP0180-NEW_BINARY_DIR = [^
+]+/project/CMP0180-NEW-build
+  CMP0180-NEW_IS_TOP_LEVEL = ON
+(-- )?  sub2proj_SOURCE_DIR = [^
+]+/project/subdir2
+  sub2proj_BINARY_DIR = [^
+]+/project/CMP0180-NEW-build/subdir2

+ 2 - 0
Tests/RunCMake/project/CMP0180-NEW.cmake

@@ -0,0 +1,2 @@
+# CMP0180 is handled in CMakeLists.txt
+include(CMP0180.cmake)

+ 16 - 0
Tests/RunCMake/project/CMP0180-OLD-stdout.txt

@@ -0,0 +1,16 @@
+(-- )?From subdir1:
+  CMP0180-OLD_SOURCE_DIR = [^
+]+/project/subdir1
+  CMP0180-OLD_BINARY_DIR = [^
+]+/project/CMP0180-OLD-build/subdir1
+  CMP0180-OLD_IS_TOP_LEVEL = OFF
+(-- )?From subdir2:
+  CMP0180-OLD_SOURCE_DIR = [^
+]+/project/subdir1
+  CMP0180-OLD_BINARY_DIR = [^
+]+/project/CMP0180-OLD-build/subdir1
+  CMP0180-OLD_IS_TOP_LEVEL = OFF
+(-- )?  sub2proj_SOURCE_DIR = [^
+]+/project/subdir2
+  sub2proj_BINARY_DIR = [^
+]+/project/CMP0180-OLD-build/subdir2

+ 2 - 0
Tests/RunCMake/project/CMP0180-OLD.cmake

@@ -0,0 +1,2 @@
+# CMP0180 is handled in CMakeLists.txt
+include(CMP0180.cmake)

+ 0 - 0
Tests/RunCMake/project/SameProjectVarsSubdir.cmake → Tests/RunCMake/project/CMP0180.cmake


+ 6 - 0
Tests/RunCMake/project/CMakeLists.txt

@@ -4,6 +4,12 @@ elseif(RunCMake_TEST MATCHES "^CMP0048")
   cmake_minimum_required(VERSION 2.8.12) # old enough to not set CMP0048
 else()
   cmake_minimum_required(VERSION 3.10)
+  # CMP0180 needs to be set before the project() call for these tests
+  if("x${RunCMake_TEST}" STREQUAL "xCMP0180-NEW")
+    cmake_policy(SET CMP0180 NEW)
+  elseif("x${RunCMake_TEST}" STREQUAL "xCMP0180-OLD")
+    cmake_policy(SET CMP0180 OLD)
+  endif()
 endif()
 project(${RunCMake_TEST} NONE)
 include(${RunCMake_TEST}.cmake)

+ 6 - 2
Tests/RunCMake/project/RunCMakeTest.cmake

@@ -63,9 +63,13 @@ run_cmake(CMP0096-OLD)
 run_cmake(CMP0096-NEW)
 
 # We deliberately run these twice to verify behavior of the second CMake run
-run_cmake(SameProjectVarsSubdir)
+run_cmake(CMP0180-OLD)
 set(RunCMake_TEST_NO_CLEAN 1)
-run_cmake(SameProjectVarsSubdir)
+run_cmake(CMP0180-OLD)
+set(RunCMake_TEST_NO_CLEAN 0)
+run_cmake(CMP0180-NEW)
+set(RunCMake_TEST_NO_CLEAN 1)
+run_cmake(CMP0180-NEW)
 set(RunCMake_TEST_NO_CLEAN 0)
 
 run_cmake(NoMinimumRequired)

+ 0 - 9
Tests/RunCMake/project/SameProjectVarsSubdir-stdout.txt

@@ -1,9 +0,0 @@
-(-- )?  SameProjectVarsSubdir_SOURCE_DIR = [^
-]+/subdir1
-  SameProjectVarsSubdir_BINARY_DIR = [^
-]+/subdir1
-  SameProjectVarsSubdir_IS_TOP_LEVEL = OFF
-(-- )?  sub2proj_SOURCE_DIR = [^
-]+/subdir2
-  sub2proj_BINARY_DIR = [^
-]+/subdir2

+ 7 - 0
Tests/RunCMake/project/subdir1/CMakeLists.txt

@@ -1 +1,8 @@
 project(${RunCMake_TEST} LANGUAGES NONE)
+
+message(STATUS
+  "From subdir1:\n"
+  "  ${RunCMake_TEST}_SOURCE_DIR = ${${RunCMake_TEST}_SOURCE_DIR}\n"
+  "  ${RunCMake_TEST}_BINARY_DIR = ${${RunCMake_TEST}_BINARY_DIR}\n"
+  "  ${RunCMake_TEST}_IS_TOP_LEVEL = ${${RunCMake_TEST}_IS_TOP_LEVEL}"
+)

+ 1 - 0
Tests/RunCMake/project/subdir2/CMakeLists.txt

@@ -1,4 +1,5 @@
 message(STATUS
+  "From subdir2:\n"
   "  ${RunCMake_TEST}_SOURCE_DIR = ${${RunCMake_TEST}_SOURCE_DIR}\n"
   "  ${RunCMake_TEST}_BINARY_DIR = ${${RunCMake_TEST}_BINARY_DIR}\n"
   "  ${RunCMake_TEST}_IS_TOP_LEVEL = ${${RunCMake_TEST}_IS_TOP_LEVEL}"