From 3c314349d5ced55d8ac6658b33c237ad0af67dfe Mon Sep 17 00:00:00 2001 From: Tyler Yankee Date: Thu, 26 Feb 2026 11:28:27 -0500 Subject: [PATCH] presets: Evaluate `fileDir` macro ahead of schedule Introduce a mechanism for "immediate" evaluation of preset macros as they are defined in the current JSON file, rather than deferring until after the preset graph is merged. This allows `${fileDir}` to always be expanded with the path to the directory containing the file which contains the macro, instead of the path being dependent on the preset in which the macro is used (which could be defined and inherited from elsewhere). Bump to presets version 12 to provide backwards compatibility with the old behavior. Fixes: #26982 Relates: #27329, #27639 --- Help/manual/cmake-presets.7.rst | 55 ++++++++++++++++--- Help/manual/presets/schema.json | 19 +++++++ Help/release/dev/presets-fileDir.rst | 7 +++ Source/cmCMakePresetsGraph.cxx | 31 +++++++++++ Source/cmCMakePresetsGraphInternal.h | 44 +++++++++++++++ Source/cmCMakePresetsGraphReadJSON.cxx | 33 ++++++++++- Tests/RunCMake/CMakePresets/FileDir.cmake | 3 - Tests/RunCMake/CMakePresets/FileDir.json.in | 11 +++- Tests/RunCMake/CMakePresets/FileDirv12.cmake | 6 ++ Tests/RunCMake/CMakePresets/FileDirv4.cmake | 9 +++ .../CMakePresets/HighVersion-stderr.txt | 2 +- .../CMakePresets/LowVersion-stderr.txt | 2 +- .../RunCMake/CMakePresets/RunCMakeTest.cmake | 16 +++++- .../subdir/FileDirInclude.json.in | 13 +++++ .../CMakePresets/subdir/FileDirv12.json.in | 17 ++++++ .../{FileDir.json.in => FileDirv4.json.in} | 5 +- 16 files changed, 254 insertions(+), 19 deletions(-) create mode 100644 Help/release/dev/presets-fileDir.rst delete mode 100644 Tests/RunCMake/CMakePresets/FileDir.cmake create mode 100644 Tests/RunCMake/CMakePresets/FileDirv12.cmake create mode 100644 Tests/RunCMake/CMakePresets/FileDirv4.cmake create mode 100644 Tests/RunCMake/CMakePresets/subdir/FileDirInclude.json.in create mode 100644 Tests/RunCMake/CMakePresets/subdir/FileDirv12.json.in rename Tests/RunCMake/CMakePresets/subdir/{FileDir.json.in => FileDirv4.json.in} (60%) diff --git a/Help/manual/cmake-presets.7.rst b/Help/manual/cmake-presets.7.rst index 8bbee0fa8f..c97efa0e45 100644 --- a/Help/manual/cmake-presets.7.rst +++ b/Help/manual/cmake-presets.7.rst @@ -1257,12 +1257,14 @@ Macro Expansion ^^^^^^^^^^^^^^^ As mentioned above, some fields support macro expansion. Macros are -recognized in the form ``${}``. All macros are -evaluated in the context of the preset being used, even if the macro is in a -field that was inherited from another preset. For example, if the ``Base`` -preset sets variable ``PRESET_NAME`` to ``${presetName}``, and the -``Derived`` preset inherits from ``Base``, ``PRESET_NAME`` will be set to -``Derived``. +recognized in the form ``${}``. + +In general, macros are evaluated in the context of the preset being used, even +if the macro is in a field that was inherited from another preset. For example, +if the ``Base`` preset sets variable ``PRESET_NAME`` to ``${presetName}``, and +the ``Derived`` preset inherits from ``Base``, ``PRESET_NAME`` will be set to +``Derived``. The ``${fileDir}`` macro as of preset version ``12`` is an +exception to this rule. It is an error to not put a closing brace at the end of a macro name. For example, ``${sourceDir`` is invalid. A dollar sign (``$``) followed by @@ -1304,9 +1306,38 @@ Recognized macros include: .. _`CMakePresets fileDir`: ``${fileDir}`` - Path to the directory containing the preset file which contains the macro. + Path to the directory containing the preset file which defines the preset + being used. + This is allowed in preset files specifying version ``4`` or above. + In preset files specifying version ``12`` or above, this *always* expands to + the directory of the current preset file containing the macro, regardless + of the preset being used. + + For example, consider the following scenario. + + * ``/path/to/CMakePresets.json`` includes + ``/path/to/subdir/CMakePresets.json``. + * ``/path/to/subdir/CMakePresets.json`` defines preset ``Base``, which + sets variable ``MY_DIR`` to ``${fileDir}``. + * ``/path/to/CMakePresets.json`` defines preset ``Derived``, and + ``Derived`` inherits from ``Base``. + + Under preset versions 4-11, ``MY_DIR`` will be set to ``/path/to/`` when + using the ``Base`` preset, and ``/path/to/subdir/`` when using the + ``Derived`` preset. + When ``/path/to/subdir/CMakePresets.json`` specifies version ``12`` or + above, ``MY_DIR`` will always be set to ``/path/to/subdir/``, regardless of + the preset being used. + + .. note:: + + Since the ``${fileDir}`` macro in version 12 is expanded in the context of + the current preset file, it is the version of the current file, rather than + the version of the root file containing the preset being used, which + enables this alternative behavior. + ``${dollar}`` A literal dollar sign (``$``). @@ -1472,6 +1503,16 @@ they were added and a summary of the new features and changes is given below. representing :option:`--parallel ` with ```` omitted. + ``12`` + .. versionadded:: 4.4 + + * Changes to `Macro Expansion`_ + + * The `${fileDir} `_ macro now always expands to + the directory of preset file containing the ``${fileDir}`` macro, + regardless of whether it is inherited by another preset in a different + directory. + Schema ====== diff --git a/Help/manual/presets/schema.json b/Help/manual/presets/schema.json index 6b60967e8c..c414ae5e98 100644 --- a/Help/manual/presets/schema.json +++ b/Help/manual/presets/schema.json @@ -180,6 +180,25 @@ "include": { "$ref": "#/definitions/include" } }, "additionalProperties": false + }, + { + "properties": { + "$schema": { "$ref": "#/definitions/$schema" }, + "$comment": { "$ref": "#/definitions/$comment" }, + "version": { + "const": 12, + "description": "A required integer representing the version of the JSON schema." + }, + "cmakeMinimumRequired": { "$ref": "#/definitions/cmakeMinimumRequiredV10" }, + "vendor": { "$ref": "#/definitions/vendor" }, + "configurePresets": { "$ref": "#/definitions/configurePresetsV10" }, + "buildPresets": { "$ref": "#/definitions/buildPresetsV11" }, + "testPresets": { "$ref": "#/definitions/testPresetsV11" }, + "packagePresets": { "$ref": "#/definitions/packagePresetsV10" }, + "workflowPresets": { "$ref": "#/definitions/workflowPresetsV10" }, + "include": { "$ref": "#/definitions/include" } + }, + "additionalProperties": false } ], "required": [ diff --git a/Help/release/dev/presets-fileDir.rst b/Help/release/dev/presets-fileDir.rst new file mode 100644 index 0000000000..45f41d29a5 --- /dev/null +++ b/Help/release/dev/presets-fileDir.rst @@ -0,0 +1,7 @@ +presets-fileDir +--------------- + +* :manual:`cmake-presets(7)` files now support schema version 12. The + :ref:`${fileDir} ` macro now always expands to the + directory of the preset file containing the ``${fileDir}`` macro, regardless + of whether it is inherited by another preset in a different directory. diff --git a/Source/cmCMakePresetsGraph.cxx b/Source/cmCMakePresetsGraph.cxx index 1959f52116..a3fb908977 100644 --- a/Source/cmCMakePresetsGraph.cxx +++ b/Source/cmCMakePresetsGraph.cxx @@ -27,6 +27,8 @@ case ExpandMacroResult::Ignore: \ out.reset(); \ return true; \ + case ExpandMacroResult::Defer: \ + CM_FALLTHROUGH; \ case ExpandMacroResult::Ok: \ break; \ } \ @@ -54,6 +56,9 @@ using BaseMacroExpander = cmCMakePresetsGraphInternal::BaseMacroExpander; template using PresetMacroExpander = cmCMakePresetsGraphInternal::PresetMacroExpander; +template +using ImmediateMacroExpander = + cmCMakePresetsGraphInternal::ImmediateMacroExpander; using cmCMakePresetsGraphInternal::ExpandMacros; void InheritString(std::string& child, std::string const& parent) @@ -453,6 +458,8 @@ bool ExpandMacros(cmCMakePresetsGraph* graph, T const& preset, case ExpandMacroResult::Ignore: out.reset(); return true; + case ExpandMacroResult::Defer: + CM_FALLTHROUGH; case ExpandMacroResult::Ok: break; } @@ -498,6 +505,30 @@ ExpandMacroResult VisitEnv(std::string& value, CycleStatus& status, } } +template +bool cmCMakePresetsGraphInternal::ExpandImmediateMacros(T& preset) +{ + MacroExpanderVector macroExpanders{}; + macroExpanders.push_back(cm::make_unique>(preset)); + cm::optional out = preset; + bool result = CheckExpandMacros(preset, out, macroExpanders); + if (out.has_value()) { + preset = out.value(); + }; + return result; +} + +template bool cmCMakePresetsGraphInternal::ExpandImmediateMacros< + ConfigurePreset>(ConfigurePreset&); +template bool cmCMakePresetsGraphInternal::ExpandImmediateMacros( + BuildPreset&); +template bool cmCMakePresetsGraphInternal::ExpandImmediateMacros( + TestPreset&); +template bool cmCMakePresetsGraphInternal::ExpandImmediateMacros< + PackagePreset>(PackagePreset&); +template bool cmCMakePresetsGraphInternal::ExpandImmediateMacros< + WorkflowPreset>(WorkflowPreset&); + ExpandMacroResult cmCMakePresetsGraphInternal::ExpandMacros( std::string& out, MacroExpanderVector const& macroExpanders, int version) { diff --git a/Source/cmCMakePresetsGraphInternal.h b/Source/cmCMakePresetsGraphInternal.h index 02c99eeb72..82d4514209 100644 --- a/Source/cmCMakePresetsGraphInternal.h +++ b/Source/cmCMakePresetsGraphInternal.h @@ -25,6 +25,7 @@ enum class ExpandMacroResult { Ok, Ignore, + Defer, Error, }; @@ -125,6 +126,49 @@ public: } }; +template +class ImmediateMacroExpander : public MacroExpander +{ + T const& Preset; + +public: + ImmediateMacroExpander(T const& preset) + : Preset(preset) + { + } + ExpandMacroResult operator()(std::string const& macroNamespace, + std::string const& macroName, + std::string& macroOut, + int version) const override + { + if (macroNamespace.empty()) { + if (macroName == "fileDir") { + if (version < 12) { + return ExpandMacroResult::Defer; + } + macroOut += + cmSystemTools::GetParentDirectory(Preset.OriginFile->Filename); + return ExpandMacroResult::Ok; + } + } + return ExpandMacroResult::Defer; + } +}; + +template +bool ExpandImmediateMacros(T& preset); + +extern template bool ExpandImmediateMacros< + cmCMakePresetsGraph::ConfigurePreset>(cmCMakePresetsGraph::ConfigurePreset&); +extern template bool ExpandImmediateMacros( + cmCMakePresetsGraph::BuildPreset&); +extern template bool ExpandImmediateMacros( + cmCMakePresetsGraph::TestPreset&); +extern template bool ExpandImmediateMacros( + cmCMakePresetsGraph::PackagePreset&); +extern template bool ExpandImmediateMacros< + cmCMakePresetsGraph::WorkflowPreset>(cmCMakePresetsGraph::WorkflowPreset&); + class NullCondition : public cmCMakePresetsGraph::Condition { bool Evaluate(MacroExpanderVector const& /*expanders*/, int /*version*/, diff --git a/Source/cmCMakePresetsGraphReadJSON.cxx b/Source/cmCMakePresetsGraphReadJSON.cxx index d141b10086..55fd2a14d7 100644 --- a/Source/cmCMakePresetsGraphReadJSON.cxx +++ b/Source/cmCMakePresetsGraphReadJSON.cxx @@ -38,9 +38,10 @@ using MacroExpander = cmCMakePresetsGraphInternal::MacroExpander; using MacroExpanderVector = cmCMakePresetsGraphInternal::MacroExpanderVector; using cmCMakePresetsGraphInternal::BaseMacroExpander; using cmCMakePresetsGraphInternal::ExpandMacros; +using cmCMakePresetsGraphInternal::ExpandImmediateMacros; constexpr int MIN_VERSION = 1; -constexpr int MAX_VERSION = 11; +constexpr int MAX_VERSION = 12; struct CMakeVersion { @@ -591,6 +592,12 @@ bool cmCMakePresetsGraph::ReadJSONFile(std::string const& filename, return false; } + if (!ExpandImmediateMacros(preset)) { + cmCMakePresetsErrors::INVALID_MACRO_EXPANSION(preset.Name, + &this->parseState); + return false; + } + PresetPair presetPair; presetPair.Unexpanded = preset; presetPair.Expanded = cm::nullopt; @@ -642,6 +649,12 @@ bool cmCMakePresetsGraph::ReadJSONFile(std::string const& filename, return false; } + if (!ExpandImmediateMacros(preset)) { + cmCMakePresetsErrors::INVALID_MACRO_EXPANSION(preset.Name, + &this->parseState); + return false; + } + PresetPair presetPair; presetPair.Unexpanded = preset; presetPair.Expanded = cm::nullopt; @@ -666,6 +679,12 @@ bool cmCMakePresetsGraph::ReadJSONFile(std::string const& filename, return false; } + if (!ExpandImmediateMacros(preset)) { + cmCMakePresetsErrors::INVALID_MACRO_EXPANSION(preset.Name, + &this->parseState); + return false; + } + PresetPair presetPair; presetPair.Unexpanded = preset; presetPair.Expanded = cm::nullopt; @@ -710,6 +729,12 @@ bool cmCMakePresetsGraph::ReadJSONFile(std::string const& filename, return false; } + if (!ExpandImmediateMacros(preset)) { + cmCMakePresetsErrors::INVALID_MACRO_EXPANSION(preset.Name, + &this->parseState); + return false; + } + PresetPair presetPair; presetPair.Unexpanded = preset; presetPair.Expanded = cm::nullopt; @@ -731,6 +756,12 @@ bool cmCMakePresetsGraph::ReadJSONFile(std::string const& filename, return false; } + if (!ExpandImmediateMacros(preset)) { + cmCMakePresetsErrors::INVALID_MACRO_EXPANSION(preset.Name, + &this->parseState); + return false; + } + PresetPair presetPair; presetPair.Unexpanded = preset; presetPair.Expanded = cm::nullopt; diff --git a/Tests/RunCMake/CMakePresets/FileDir.cmake b/Tests/RunCMake/CMakePresets/FileDir.cmake deleted file mode 100644 index c7298cdaed..0000000000 --- a/Tests/RunCMake/CMakePresets/FileDir.cmake +++ /dev/null @@ -1,3 +0,0 @@ -include(${CMAKE_CURRENT_LIST_DIR}/TestVariable.cmake) - -test_variable(TEST_FILE_DIR "" "${CMAKE_CURRENT_SOURCE_DIR}/subdir") diff --git a/Tests/RunCMake/CMakePresets/FileDir.json.in b/Tests/RunCMake/CMakePresets/FileDir.json.in index 899e5f302b..b7418535d4 100644 --- a/Tests/RunCMake/CMakePresets/FileDir.json.in +++ b/Tests/RunCMake/CMakePresets/FileDir.json.in @@ -1,6 +1,15 @@ { "version": 4, "include": [ - "subdir/FileDir.json" + "@FileDir_include_subdir@" + ], + "configurePresets": [ + { + "name": "FileDirExt", + "inherits": "FileDir", + "cacheVariables": { + "PRESET": "FileDirExt" + } + } ] } diff --git a/Tests/RunCMake/CMakePresets/FileDirv12.cmake b/Tests/RunCMake/CMakePresets/FileDirv12.cmake new file mode 100644 index 0000000000..04fb6e9b8f --- /dev/null +++ b/Tests/RunCMake/CMakePresets/FileDirv12.cmake @@ -0,0 +1,6 @@ +include(${CMAKE_CURRENT_LIST_DIR}/TestVariable.cmake) + +if(NOT "${PRESET}" STREQUAL "FileDirInclude") + test_variable(CMAKE_BINARY_DIR "" "${CMAKE_CURRENT_SOURCE_DIR}/subdir/build") + test_variable(TEST_FILE_DIR "" "${CMAKE_CURRENT_SOURCE_DIR}/subdir") +endif() diff --git a/Tests/RunCMake/CMakePresets/FileDirv4.cmake b/Tests/RunCMake/CMakePresets/FileDirv4.cmake new file mode 100644 index 0000000000..32f516c9cb --- /dev/null +++ b/Tests/RunCMake/CMakePresets/FileDirv4.cmake @@ -0,0 +1,9 @@ +include(${CMAKE_CURRENT_LIST_DIR}/TestVariable.cmake) + +if (PRESET STREQUAL "FileDir") + test_variable(CMAKE_BINARY_DIR "" "${CMAKE_CURRENT_SOURCE_DIR}/subdir/build") + test_variable(TEST_FILE_DIR "" "${CMAKE_CURRENT_SOURCE_DIR}/subdir") +elseif (PRESET STREQUAL "FileDirExt") + test_variable(CMAKE_BINARY_DIR "" "${CMAKE_CURRENT_SOURCE_DIR}/build") + test_variable(TEST_FILE_DIR "" "${CMAKE_CURRENT_SOURCE_DIR}") +endif() diff --git a/Tests/RunCMake/CMakePresets/HighVersion-stderr.txt b/Tests/RunCMake/CMakePresets/HighVersion-stderr.txt index ba962b44f4..4a9f6e4f84 100644 --- a/Tests/RunCMake/CMakePresets/HighVersion-stderr.txt +++ b/Tests/RunCMake/CMakePresets/HighVersion-stderr.txt @@ -1,5 +1,5 @@ ^CMake Error: Could not read presets from [^ ]*/Tests/RunCMake/CMakePresets/HighVersion: -CMakePresets\.json:2: Unrecognized "version" 1000: must be >=1 and <=11 +CMakePresets\.json:2: Unrecognized "version" 1000: must be >=1 and <=12 "version": 1000, \^$ diff --git a/Tests/RunCMake/CMakePresets/LowVersion-stderr.txt b/Tests/RunCMake/CMakePresets/LowVersion-stderr.txt index 5ed2857ab8..880058fc75 100644 --- a/Tests/RunCMake/CMakePresets/LowVersion-stderr.txt +++ b/Tests/RunCMake/CMakePresets/LowVersion-stderr.txt @@ -1,5 +1,5 @@ ^CMake Error: Could not read presets from [^ ]*/Tests/RunCMake/CMakePresets/LowVersion: -CMakePresets\.json:2: Unrecognized "version" 0: must be >=1 and <=11 +CMakePresets\.json:2: Unrecognized "version" 0: must be >=1 and <=12 "version": 0, \^ diff --git a/Tests/RunCMake/CMakePresets/RunCMakeTest.cmake b/Tests/RunCMake/CMakePresets/RunCMakeTest.cmake index 6ffd7e5b90..80999b7bcb 100644 --- a/Tests/RunCMake/CMakePresets/RunCMakeTest.cmake +++ b/Tests/RunCMake/CMakePresets/RunCMakeTest.cmake @@ -377,10 +377,20 @@ run_cmake_presets(HostSystemNameFuture) # Test ${fileDir} macro set(CMakePresets_FILE "${RunCMake_SOURCE_DIR}/FileDir.json.in") +set(FileDir_include_subdir "subdir/FileDirv4.json") set(CMakePresets_EXTRA_FILES - "${RunCMake_SOURCE_DIR}/subdir/FileDir.json.in" - ) -run_cmake_presets(FileDir) + "${RunCMake_SOURCE_DIR}/${FileDir_include_subdir}.in" +) +run_cmake_presets(FileDirv4 --preset FileDir) +run_cmake_presets(FileDirv4 --preset FileDirExt) +set(FileDir_include_subdir "subdir/FileDirv12.json") +set(CMakePresets_EXTRA_FILES + "${RunCMake_SOURCE_DIR}/${FileDir_include_subdir}.in" + "${RunCMake_SOURCE_DIR}/subdir/FileDirInclude.json.in" +) +run_cmake_presets(FileDirv12 --preset FileDir) +run_cmake_presets(FileDirv12 --preset FileDirExt) +run_cmake_presets(FileDirv12 --preset FileDirInclude) unset(CMakePresets_EXTRA_FILES) set(CMakePresets_FILE "${RunCMake_SOURCE_DIR}/FileDirFuture.json.in") run_cmake_presets(FileDirFuture) diff --git a/Tests/RunCMake/CMakePresets/subdir/FileDirInclude.json.in b/Tests/RunCMake/CMakePresets/subdir/FileDirInclude.json.in new file mode 100644 index 0000000000..f7dd5972a3 --- /dev/null +++ b/Tests/RunCMake/CMakePresets/subdir/FileDirInclude.json.in @@ -0,0 +1,13 @@ +{ + "version": 9, + "configurePresets": [ + { + "name": "FileDirInclude", + "generator": "@RunCMake_GENERATOR@", + "binaryDir": "${fileDir}/build", + "cacheVariables": { + "PRESET": "FileDirInclude" + } + } + ] +} diff --git a/Tests/RunCMake/CMakePresets/subdir/FileDirv12.json.in b/Tests/RunCMake/CMakePresets/subdir/FileDirv12.json.in new file mode 100644 index 0000000000..ed20764f81 --- /dev/null +++ b/Tests/RunCMake/CMakePresets/subdir/FileDirv12.json.in @@ -0,0 +1,17 @@ +{ + "version": 12, + "include": [ + "${fileDir}/FileDirInclude.json" + ], + "configurePresets": [ + { + "name": "FileDir", + "generator": "@RunCMake_GENERATOR@", + "binaryDir": "${fileDir}/build", + "cacheVariables": { + "TEST_FILE_DIR": "${fileDir}", + "PRESET": "FileDir" + } + } + ] +} diff --git a/Tests/RunCMake/CMakePresets/subdir/FileDir.json.in b/Tests/RunCMake/CMakePresets/subdir/FileDirv4.json.in similarity index 60% rename from Tests/RunCMake/CMakePresets/subdir/FileDir.json.in rename to Tests/RunCMake/CMakePresets/subdir/FileDirv4.json.in index 00282a7b81..e055c4e30e 100644 --- a/Tests/RunCMake/CMakePresets/subdir/FileDir.json.in +++ b/Tests/RunCMake/CMakePresets/subdir/FileDirv4.json.in @@ -4,9 +4,10 @@ { "name": "FileDir", "generator": "@RunCMake_GENERATOR@", - "binaryDir": "${sourceDir}/build", + "binaryDir": "${fileDir}/build", "cacheVariables": { - "TEST_FILE_DIR": "${fileDir}" + "TEST_FILE_DIR": "${fileDir}", + "PRESET": "FileDir" } } ]