Переглянути джерело

Updated o3de CLI register command to generate a CMakePresets file for project registration (#15984)

* Updated o3de CLI register command to generate a CMakePresets file for project registration

When a project is registered a
`<project-root>/user/cmake/engine/CmakePresets.json` will be
added/updated which contains an "include" entry to the registered
engine's ``CMakePresets.json` file

The Default and Minimal Project templates have had their
`<project-root>/CMakePresets.json` file to include the generated
"user/cmake/engine/CMakePresets.json" file.

This means that new projects will now opt-in to this functionality by
default.

Added a CMakePresets.json file for the AutomatedTesting project that
includes the file in the CMakePresets.json it's engine root directory.

Signed-off-by: lumberyard-employee-dm <[email protected]>

* Added support to the o3de cmake.py script to expose `update_cmake_presets_for_project` as command

With the `update_cmake_presets_for_project` method exposed as python
command of `update-cmake-presets-for-project`, the CMake configuration
logic was updated as well to invoke that command to update the any
active projects "user/cmake/engine/CMakePresets.json" with an include
entry that points to the engines CMakePresets.json

Signed-off-by: lumberyard-employee-dm <[email protected]>

* Update cmake/Projects.cmake

Updated comment for the `install_project_asset_artifacts` to indicate that it is only adding the cmake logic to perform the archiving of the project cache directory when the `install` step occurs.

Co-authored-by: Alex Peterson <[email protected]>
Signed-off-by: lumberyard-employee-dm <[email protected]>

---------

Signed-off-by: lumberyard-employee-dm <[email protected]>
Co-authored-by: Alex Peterson <[email protected]>
lumberyard-employee-dm 2 роки тому
батько
коміт
a9511c52cd

+ 11 - 0
AutomatedTesting/CMakePresets.json

@@ -0,0 +1,11 @@
+{
+    "version": 4,
+    "cmakeMinimumRequired": {
+        "major": 3,
+        "minor": 23,
+        "patch": 0
+    },
+    "include": [
+        "../CMakePresets.json"
+    ]
+}

+ 4 - 1
Templates/DefaultProject/Template/CMakePresets.json

@@ -4,5 +4,8 @@
         "major": 3,
         "minor": 23,
         "patch": 0
-    }
+    },
+    "include": [
+        "user/cmake/engine/CMakePresets.json"
+    ]
 }

+ 4 - 1
Templates/MinimalProject/Template/CMakePresets.json

@@ -4,5 +4,8 @@
         "major": 3,
         "minor": 23,
         "patch": 0
-    }
+    },
+    "include": [
+        "user/cmake/engine/CMakePresets.json"
+    ]
 }

+ 31 - 0
cmake/Projects.cmake

@@ -233,6 +233,31 @@ endif()
     ly_install_run_code("${install_engine_pak_code}")
 endfunction()
 
+#! Updates a generated <project-path>/user/cmake/engine/CMakePresets.json
+# file to include the path to the engine root CMakePresets.json
+# \arg: PROJECT_PATH path to project root.
+#       will used to form the root of the file path to the presets file that includes the engine presets
+# \arg: ENGINE_PATH path to the engine root
+function(update_cmake_presets_for_project)
+    set(options)
+    set(oneValueArgs PROJECT_PATH ENGINE_PATH)
+    set(multiValueArgs)
+    cmake_parse_arguments("${CMAKE_CURRENT_FUNCTION}" "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
+    set(project_path "${${CMAKE_CURRENT_FUNCTION}_PROJECT_PATH}")
+    set(engine_path "${${CMAKE_CURRENT_FUNCTION}_ENGINE_PATH}")
+
+    execute_process(COMMAND
+        ${LY_PYTHON_CMD} "${LY_ROOT_FOLDER}/scripts/o3de/o3de/cmake.py" "update-cmake-presets-for-project" -pp "${project_path}" -ep "${engine_path}"
+        WORKING_DIRECTORY ${LY_ROOT_FOLDER}
+        RESULT_VARIABLE O3DE_CLI_RESULT
+        ERROR_VARIABLE O3DE_CLI_ERROR
+        )
+
+    if(NOT O3DE_CLI_RESULT EQUAL 0)
+        message(STATUS "Unable to update the project \"${project_path}\" CMakePresets to include the engine presets:\n${O3DE_CLI_ERROR}")
+    endif()
+endfunction()
+
 # Add the projects here so the above function is found
 foreach(project ${LY_PROJECTS})
     file(REAL_PATH ${project} full_directory_path BASE_DIRECTORY ${CMAKE_SOURCE_DIR})
@@ -260,8 +285,14 @@ foreach(project ${LY_PROJECTS})
     # Append the project external directory to LY_EXTERNAL_SUBDIR_${project_name} property
     add_project_json_external_subdirectories(${full_directory_path} "${project_name}")
 
+    # Use the install(CODE) command to archive the project cache
+    # directory assets for use in a proejct relase layout
     install_project_asset_artifacts(${full_directory_path})
 
+    # Update the <project-path>/user/cmake/engine/CMakePresets.json
+    # to include the current engine CMakePresets.json file
+    update_cmake_presets_for_project(PROJECT_PATH "${full_directory_path}" ENGINE_PATH "${LY_ROOT_FOLDER}")
+
 endforeach()
 
 # If just one project is defined we pass it as a parameter to the applications

+ 2 - 2
cmake/Subdirectories.cmake

@@ -207,7 +207,7 @@ function(resolve_gem_dependencies object_type object_path)
         set(user_external_subdir_option -ed "${user_external_subdirs}")
     endif()
     execute_process(COMMAND 
-        ${LY_PYTHON_CMD} "${LY_ROOT_FOLDER}/scripts/o3de/o3de/cmake.py" --${object_type_lower}-path "${object_path}" --engine-path "${LY_ROOT_FOLDER}" ${user_external_subdir_option}
+        ${LY_PYTHON_CMD} "${LY_ROOT_FOLDER}/scripts/o3de/o3de/cmake.py" "resolve-gem-dependencies" --${object_type_lower}-path "${object_path}" --engine-path "${LY_ROOT_FOLDER}" ${user_external_subdir_option}
         WORKING_DIRECTORY ${LY_ROOT_FOLDER}
         RESULT_VARIABLE O3DE_CLI_RESULT
         OUTPUT_VARIABLE resolved_gem_dependency_output 
@@ -215,7 +215,7 @@ function(resolve_gem_dependencies object_type object_path)
         )
 
     if(O3DE_CLI_RESULT)
-        message(WARNING "Dependecy resolution failed.\n  If needed, set the O3DE_DISABLE_GEM_DEPENDENCY_RESOLUTION variable to bypass dependency resolution.\n  Error: ${O3DE_CLI_OUT}")
+        message(WARNING "Dependency resolution failed.\n  If needed, set the O3DE_DISABLE_GEM_DEPENDENCY_RESOLUTION variable to bypass dependency resolution.\n  Error: ${O3DE_CLI_OUT}")
         return()
     endif()
 

+ 170 - 16
scripts/o3de/o3de/cmake.py

@@ -10,15 +10,112 @@ Contains methods for query CMake gem target information
 """
 
 import argparse
+import enum
+import json
 import logging
 import pathlib
+import string
 import sys
+from typing import Tuple
 
 from o3de import manifest, utils, compatibility
 
 logger = logging.getLogger('o3de.cmake')
 logging.basicConfig(format=utils.LOG_FORMAT)
 
+TEMPLATE_CMAKE_PRESETS_INCLUDE_JSON = """
+{
+    "version": 4,
+    "cmakeMinimumRequired": {
+        "major": 3,
+        "minor": 23,
+        "patch": 0
+    },
+    "include": [
+        "${CMakePresetsInclude}"
+    ]
+}
+"""
+
+PROJECT_ENGINE_PRESET_RELATIVE_PATH = pathlib.PurePath('user/cmake/engine/CMakePresets.json')
+
+class UpdatePresetResult(enum.Enum):
+    EnginePathAdded = 0
+    EnginePathAlreadyIncluded = 1
+    Error = 2
+
+def update_cmake_presets_for_project(preset_path: pathlib.PurePath, engine_name: str = '',
+                                     engine_version: str = '',
+                                     engine_path: pathlib.PurePath or None = None) -> UpdatePresetResult:
+    """
+    Updates a cmake-presets formated JSON file with an include that points
+    to the root CMakePresets.json inside the registered engine
+    :param preset_path: path to the file to update with cmake-preset formatted json
+    :param engine_name: name of the engine
+    :param engine_version: version specifier for the engine.
+           if empty string it is not used
+    :return: UpdatePresetResult enum with value EnginePathAdded or EnginePathAlreadyIncluded
+             if successful
+    """
+    if not engine_path:
+        engine_with_specifier = f'{engine_name}=={engine_version}' if engine_version else engine_name
+        engine_path = manifest.get_registered(engine_name=engine_with_specifier)
+        if not engine_path:
+            logger.error(f'Engine with identifier {engine_with_specifier} is not registered.\n'
+                         f'The cmake-presets file at {preset_path} will not be modified')
+            return UpdatePresetResult.Error
+
+    engine_cmake_presets_path = engine_path / "CMakePresets.json"
+    preset_json = {}
+    # Convert the path to a concrete Path option
+    preset_path = pathlib.Path(preset_path)
+    try:
+        with preset_path.open('r') as preset_fp:
+            try:
+                preset_json = json.load(preset_fp)
+            except json.JSONDecodeError as e:
+                logger.warning(f'Cannot parse JSON data from cmake-presets file at path "{preset_path}".\n'
+                            'The JSON content in the file will be reset to only include the path to the registered engine:\n'
+                            f'{str(e)}')
+    except OSError as e:
+        # It is OK if the preset_path file does not exist
+        pass
+
+    # Update an existing preset file if it exist
+    if preset_json:
+        preset_include_list = preset_json.get('include', [])
+        if engine_cmake_presets_path in map(lambda preset_json_include: pathlib.PurePath(preset_json_include), preset_include_list):
+            # If the engine_path is already included in the preset file, return without writing to the file
+            return UpdatePresetResult.EnginePathAlreadyIncluded
+
+        # Replace all "include" paths in the existing preset file
+        # The reason this occurs is to prevent a scenario where previously registered engines
+        # are being referenced by this preset file
+        preset_json['include'] = [ engine_cmake_presets_path.as_posix() ]
+    else:
+        try:
+            preset_json = json.loads(string.Template(TEMPLATE_CMAKE_PRESETS_INCLUDE_JSON).safe_substitute(
+                CMakePresetsInclude=engine_cmake_presets_path.as_posix()))
+        except json.JSONDecodeError as e:
+            logger.error(f'Failed to substitute engine path {engine_path} into project CMake Presets template')
+            return UpdatePresetResult.Error
+
+    result = UpdatePresetResult.EnginePathAdded
+    # Write the updated cmake-presets json to the preset_path file
+    try:
+        preset_path.parent.mkdir(parents=True, exist_ok=True)
+        with preset_path.open('w') as preset_fp:
+            try:
+                preset_fp.write(json.dumps(preset_json, indent=4) + '\n')
+                return result
+            except OSError as e:
+                logger.error(f'Failed to write "{preset_path}" to filesystem: {str(e)}')
+                return UpdatePresetResult.Error
+    except OSError as e:
+        logger.error(f'Failed to open {preset_path} for write: {str(e)}')
+        return UpdatePresetResult.Error
+
+
 enable_gem_start_marker = 'set(ENABLED_GEMS'
 enable_gem_end_marker = ')'
 
@@ -105,9 +202,9 @@ def resolve_gem_dependency_paths(
     writes the output to the path provided.  This is used during CMake
     configuration because writing a CMake depencency resolver would be
     difficult and Python already has a solver with unit tests.
-    :param engine_path: optional path to the engine, if not provided, the project's engine will be determined 
+    :param engine_path: optional path to the engine, if not provided, the project's engine will be determined
     :param project_path: optional path to the project, if not provided the engine path must be provided
-    :param resolved_gem_dependencies_output_path: optional path to a file that will be written 
+    :param resolved_gem_dependencies_output_path: optional path to a file that will be written
         containing a CMake list of gem names and paths.  If not provided, the list is written to STDOUT.
     :return: 0 for success or non 0 failure code
     """
@@ -127,7 +224,7 @@ def resolve_gem_dependency_paths(
 
             logger.warning('Failed to determine the correct engine for the project at '
                            f'"{project_path}", falling back to this engine at {engine_path}.')
-    
+
     engine_json_data = manifest.get_engine_json_data(engine_path=engine_path)
     if not engine_json_data:
         logger.error('Failed to retrieve engine json data for the engine at '
@@ -147,7 +244,7 @@ def resolve_gem_dependency_paths(
     else:
         active_gem_names = engine_json_data.get('gem_names',[])
 
-    # some gem name entries will be dictionaries - convert to a set of strings 
+    # some gem name entries will be dictionaries - convert to a set of strings
     gem_names_with_optional_gems = utils.get_gem_names_set(active_gem_names, include_optional=True)
     if not gem_names_with_optional_gems:
         logger.info(f'No gem names were found to use as input to resolve gem dependencies.')
@@ -156,24 +253,24 @@ def resolve_gem_dependency_paths(
                 output.write('')
         return 0
 
-    all_gems_json_data = manifest.get_gems_json_data_by_name(engine_path=engine_path, 
-                                                             project_path=project_path, 
-                                                             include_manifest_gems=True, 
+    all_gems_json_data = manifest.get_gems_json_data_by_name(engine_path=engine_path,
+                                                             project_path=project_path,
+                                                             include_manifest_gems=True,
                                                              include_engine_gems=True,
                                                              external_subdirectories=external_subdirectories.split(';') if isinstance(external_subdirectories, str) else external_subdirectories)
 
     # First try to resolve with optional gems
-    results, errors = compatibility.resolve_gem_dependencies(gem_names_with_optional_gems, 
-                                                             all_gems_json_data, 
-                                                             engine_json_data, 
+    results, errors = compatibility.resolve_gem_dependencies(gem_names_with_optional_gems,
+                                                             all_gems_json_data,
+                                                             engine_json_data,
                                                              include_optional=True)
     if errors:
         logger.warning('Failed to resolve dependencies with optional gems, trying without optional gems.')
 
         # Try without optional gems
         gem_names_without_optional = utils.get_gem_names_set(active_gem_names, include_optional=False)
-        results, errors = compatibility.resolve_gem_dependencies(gem_names_without_optional, 
-                                                                 all_gems_json_data, 
+        results, errors = compatibility.resolve_gem_dependencies(gem_names_without_optional,
+                                                                 all_gems_json_data,
                                                                  engine_json_data,
                                                                  include_optional=False)
 
@@ -205,8 +302,37 @@ def _resolve_gem_dependency_paths(args: argparse) -> int:
                             resolved_gem_dependencies_output_path=args.gem_paths_output_file
                              )
 
-def add_parser_args(parser):
-    group = parser.add_argument_group("resolve gem dependencies")
+def _update_project_presets_to_include_engine_presets(args: argparse) -> int:
+    project_path = args.project_path
+    if not project_path:
+        project_path = manifest.get_registered(project_name=args.project_name)
+        if not project_path:
+            logger.error(f'Project with name {args.project_name} is not registered')
+            return 1
+
+    # Form the path the CMakePresets.json that will include the engine presets
+    preset_path = project_path / PROJECT_ENGINE_PRESET_RELATIVE_PATH
+
+    # Map boolean non-error result to a return code of 0
+    return 0 if update_cmake_presets_for_project(
+        preset_path=preset_path,
+        engine_name=args.engine_name,
+        engine_version=args.engine_version,
+        engine_path=args.engine_path) != UpdatePresetResult.Error else 1
+
+def add_args(subparsers) -> None:
+    """
+    add_args is called to add expected parser arguments and subparsers arguments to each command such that it can be
+    invoked locally or aggregated by a central python file.
+    Ex. Directly run from this file alone with: <engine-root>/python/python cmake.py resolve-gem-dependencies --pp <path-to-project>
+    OR
+    o3de.py can aggregate commands by importing cmake,
+    call add_args and execute:  <engine-root>/python/python o3de.py resolve-gem-dependencies --pp <path-to-project>
+    :param subparsers: the caller instantiates subparsers and passes it in here
+    """
+    # Add command for resolving gem depenencies
+    gem_dependencies_parser = subparsers.add_parser('resolve-gem-dependencies')
+    group = gem_dependencies_parser.add_argument_group("resolve gem dependencies")
     group.add_argument('-pp', '--project-path', type=pathlib.Path, required=False,
                        help='The path to the project.')
     group.add_argument('-ep', '--engine-path', type=pathlib.Path, required=False,
@@ -215,11 +341,39 @@ def add_parser_args(parser):
                        help='Additional list of subdirectories.')
     group.add_argument('-gpof', '--gem-paths-output-file', type=pathlib.Path, required=False,
                        help='The path to the resolved gem paths output file. If not provided, the list will be output to STDOUT.')
-    parser.set_defaults(func=_resolve_gem_dependency_paths)
+    gem_dependencies_parser.set_defaults(func=_resolve_gem_dependency_paths)
+
+    # Add command for updating the project presets
+    update_cmake_presets_for_project_parser = subparsers.add_parser('update-cmake-presets-for-project')
+    project_group = update_cmake_presets_for_project_parser.add_mutually_exclusive_group(required=True)
+    project_group.add_argument('--project-path', '-pp', type=pathlib.Path,
+                               help='The path to a project.')
+    project_group.add_argument('--project-name', '-pn', type=str,
+                               help='The name of a project.')
+
+    engine_group = update_cmake_presets_for_project_parser.add_argument_group('engine identifiers')
+    # The --engine-path and --engine-name arguments are mutually exclusive and one of the are required
+    engine_path_group = engine_group.add_mutually_exclusive_group(required=True)
+    engine_path_group.add_argument('--engine-path', '-ep', type=pathlib.Path,
+                                   help='The path to the engine.')
+    engine_path_group.add_argument('--engine-name', '-en', type=str,
+                                   help='The name of the engine use to lookup the engine path.')
+    # Add the --engine-version argument directly to the `engine_group` variable
+    engine_group.add_argument('--engine-version', '-ev', type=str,
+                              help='Version of the engine to query when the --engine-name argument is used')
+
+
+    update_cmake_presets_for_project_parser.set_defaults(func=_update_project_presets_to_include_engine_presets)
 
 def main():
     the_parser = argparse.ArgumentParser()
-    add_parser_args(the_parser)
+
+    script_name = pathlib.PurePath(__file__).name
+    cmake_subparsers = the_parser.add_subparsers(
+        help=f'To get help on a sub-command:\n{script_name} <sub-command> -h',
+        title='Sub-Commands', dest='command', required=True)
+    add_args(cmake_subparsers)
+
     the_args = the_parser.parse_args()
     ret = the_args.func(the_args) if hasattr(the_args, 'func') else 1
     sys.exit(ret)

+ 7 - 0
scripts/o3de/o3de/register.py

@@ -538,6 +538,13 @@ def register_project_path(json_data: dict,
             if not manifest.save_o3de_manifest(project_json_data, project_json_path):
                 return 1
 
+        if not dry_run:
+            # If the project.json engine is being updated, also update the user/engine/CMakePresets.json
+            # file to include the location CMakePresets.json in the engine for the local user
+            cmake.update_cmake_presets_for_project(preset_path=project_path / cmake.PROJECT_ENGINE_PRESET_RELATIVE_PATH,
+                                                   engine_name=project_json_data['engine'],
+                                                   engine_version=project_json_data['engine_version'])
+
     if dry_run:
         logger.info('Project path was not registered because --dry-run option was specified')
 

+ 70 - 8
scripts/o3de/tests/test_cmake.py

@@ -9,6 +9,7 @@
 import io
 import pytest
 import pathlib
+import string
 from unittest.mock import patch
 
 from o3de import cmake, manifest
@@ -143,7 +144,7 @@ class TestResolveGemDependencyPaths:
                     {"gem_name":"gemA", "version":"1.2.3", "path":pathlib.Path('gemA1Path')},
                     {"gem_name":"gemA", "version":"2.3.4", "path":pathlib.Path('gemA2Path')},
                 ]
-            }, 'gemA;gemA1Path', 0) 
+            }, 'gemA;gemA1Path', 0)
         ]
     )
     def test_resolve_gem_dependency_paths(self, engine_gem_names, project_gem_names, all_gems_json_data, expected_gem_paths, expected_result):
@@ -161,7 +162,7 @@ class TestResolveGemDependencyPaths:
             return {
                 "engine_name":"o3de",
                 "gem_names": engine_gem_names
-            } 
+            }
 
         def get_project_json_data(project_name: str = None,
                                 project_path: str or pathlib.Path = None,
@@ -170,7 +171,7 @@ class TestResolveGemDependencyPaths:
                 "project_name":"o3de_project",
                 "engine":"o3de",
                 "gem_names": project_gem_names
-            } 
+            }
 
         class StringBufferIOWrapper(io.StringIO):
             def __exit__(self, exc_type, exc_val, exc_tb):
@@ -181,13 +182,13 @@ class TestResolveGemDependencyPaths:
         def get_enabled_gem_cmake_file(project_name: str = None,
                                 project_path: str or pathlib.Path = None,
                                 platform: str = 'Common'):
-            return pathlib.Path() 
+            return pathlib.Path()
 
         def get_enabled_gems(cmake_file: pathlib.Path) -> set:
-            return set() 
+            return set()
 
-        def get_gems_json_data_by_name(engine_path:pathlib.Path = None, 
-                                       project_path: pathlib.Path = None, 
+        def get_gems_json_data_by_name(engine_path:pathlib.Path = None,
+                                       project_path: pathlib.Path = None,
                                        include_manifest_gems: bool = False,
                                        include_engine_gems: bool = False,
                                        external_subdirectories: list = None
@@ -205,10 +206,71 @@ class TestResolveGemDependencyPaths:
                 patch('o3de.manifest.get_enabled_gems', side_effect=get_enabled_gems) as get_enabled_gems_patch,\
                 patch('pathlib.Path.open', side_effect=lambda mode: StringBufferIOWrapper()) as pathlib_open_mock:
             result = cmake.resolve_gem_dependency_paths(
-                                        engine_path=engine_path if engine_gem_names else None, 
+                                        engine_path=engine_path if engine_gem_names else None,
                                         project_path=project_path if project_gem_names else None,
                                         external_subdirectories=None,
                                         resolved_gem_dependencies_output_path=pathlib.Path('out'))
 
         assert result == expected_result
         assert resolved_gem_paths == expected_gem_paths
+
+
+class TestUpdateCMakePresetsJson:
+    @pytest.mark.parametrize(
+        "preset_path, engine_name, engine_version, preset_json_content, expected_result",
+        [
+            # This should create the cmakepresets file and add the registered 'o3de' engine to it
+            pytest.param('user/CMakePresets.json', 'o3de', '', '', cmake.UpdatePresetResult.EnginePathAdded),
+            # This should create the cmake-presets file and add the registered 'o3de==1.0.0' engine to it
+            pytest.param('user/CMakePresets.json', 'o3de', '1.0.0', '',cmake.UpdatePresetResult.EnginePathAdded),
+            # This should fail to create the cmake-presets file as 'o3de==2.0.0' is not registered
+            pytest.param('user/CMakePresets.json', 'o3de', '2.0.0', '', cmake.UpdatePresetResult.Error),
+            # This should fail to create the cmake-presets file as 'o3de-not-registered' is not registered
+            pytest.param('user/CMakePresets.json', 'o3de-not-registered', '', '', cmake.UpdatePresetResult.Error),
+            # This should fail to create the cmake-presets file as the file path does not exist
+            pytest.param('', 'o3de', '1.0.0', '', cmake.UpdatePresetResult.Error),
+            # This should update an existing cmake-presets file
+            pytest.param('user/CMakePresets.json', 'o3de', '',
+                          string.Template(cmake.TEMPLATE_CMAKE_PRESETS_INCLUDE_JSON).safe_substitute(CMakePresetsInclude='<other-engine-root>/CMakePresets.json'),
+                          cmake.UpdatePresetResult.EnginePathAdded),
+            # This should update an existing cmake-presets file
+            pytest.param('user/CMakePresets.json', 'o3de', '',
+                          string.Template(cmake.TEMPLATE_CMAKE_PRESETS_INCLUDE_JSON).safe_substitute(CMakePresetsInclude='<engine-root>/CMakePresets.json'),
+                          cmake.UpdatePresetResult.EnginePathAlreadyIncluded)
+        ]
+    )
+    def test_update_cmake_presets_for_project(self, preset_path, engine_name, engine_version,
+                                              preset_json_content, expected_result):
+        class StringBufferIOWrapper(io.StringIO):
+            def __init__(self):
+                nonlocal preset_json_content
+                super().__init__(preset_json_content)
+            def __enter__(self):
+                return super().__enter__()
+            def __exit__(self, exc_type, exc_val, exc_tb):
+                nonlocal preset_json_content
+                preset_json_content = super().getvalue()
+                super().__exit__(exc_tb, exc_val, exc_tb)
+
+        def preset_json_open(mode):
+            if preset_path:
+                return StringBufferIOWrapper()
+            raise OSError('Cannot Open file "{preset_path}"')
+
+        # patch the manifest.get_registered method to return a placeholder path
+        # to the engine CMakePresets.json
+        def get_registered(engine_name) -> pathlib.PurePath or None:
+            if engine_name == 'o3de':
+                return pathlib.PurePath("<engine-root>")
+            elif engine_name == 'o3de==1.0.0':
+                return pathlib.PurePath("<engine-root2>")
+            return None
+
+
+        with patch('pathlib.Path.open', side_effect=preset_json_open) as pathlib_open_mock, \
+            patch('pathlib.Path.mkdir', return_value=True) as pathlib_mkdir_mock, \
+            patch('o3de.manifest.get_registered', side_effect=get_registered) as get_registered_mock:
+            result = cmake.update_cmake_presets_for_project(preset_path=preset_path,
+                                                   engine_name=engine_name,
+                                                   engine_version=engine_version)
+        assert result == expected_result