diff --git a/scripts/o3de/o3de/disable_gem.py b/scripts/o3de/o3de/disable_gem.py index 6c466c3631..61d71445f0 100644 --- a/scripts/o3de/o3de/disable_gem.py +++ b/scripts/o3de/o3de/disable_gem.py @@ -83,7 +83,7 @@ def disable_gem_in_project(gem_name: str = None, project_path = manifest.get_registered(project_name=project_name) if not project_path: logger.error(f'Unable to locate project path from the registered manifest.json files:' - f' {str(pathlib.Path.home() / ".o3de/manifest.json")}, engine.json') + f' {str(pathlib.Path("~/.o3de/o3de_manifest.json").expanduser())}, engine.json') return 1 project_path = pathlib.Path(project_path).resolve() diff --git a/scripts/o3de/o3de/enable_gem.py b/scripts/o3de/o3de/enable_gem.py index 73fc2ea3cf..0dee01e05e 100644 --- a/scripts/o3de/o3de/enable_gem.py +++ b/scripts/o3de/o3de/enable_gem.py @@ -117,7 +117,7 @@ def enable_gem_in_project(gem_name: str = None, gem_path = manifest.get_registered(gem_name=gem_name) if not gem_path: logger.error(f'Unable to locate gem path from the registered manifest.json files:' - f' {str(pathlib.Path.home() / ".o3de/manifest.json")},' + f' {str(pathlib.Path( "~/.o3de/o3de_manifest.json").expanduser())},' f' {project_path / "project.json"}, engine.json') return 1 diff --git a/scripts/o3de/o3de/global_project.py b/scripts/o3de/o3de/global_project.py index 787e676a7e..d165510250 100644 --- a/scripts/o3de/o3de/global_project.py +++ b/scripts/o3de/o3de/global_project.py @@ -16,94 +16,122 @@ import sys import re import pathlib import json -from o3de import manifest + +from o3de import manifest, validation logger = logging.getLogger() logging.basicConfig() +DEFAULT_BOOTSTRAP_SETREG = pathlib.Path('~/.o3de/Registry/bootstrap.setreg').expanduser() +PROJECT_PATH_KEY = ('Amazon', 'AzCore', 'Bootstrap', 'project_path') -def set_global_project(project_name: str or None, - project_path: str or pathlib.Path or None) -> int: +def get_json_data(input_path: pathlib.Path): + setreg_json_data = {} + # If the output_path exist validate that it is a valid json file + if input_path.is_file(): + with input_path.open('r') as f: + try: + setreg_json_data = json.load(f) + except json.JSONDecodeError as e: + logger.error(f'The file: {input_path} is not a valid json file: {str(e)}') + + return setreg_json_data + +def set_global_project(output_path: pathlib.Path, + project_name: str = None, + project_path: pathlib.Path = None, + force: bool = False) -> int: """ - set what the current project is - :param project_name: the name of the project you want to set, resolves project_path - :param project_path: the path of the project you want to set + Adds a project path the a settings registry file in the users ~/.o3de/Registry directory + :param output_path: path to .setreg file to store project_path value into + :param project_name: name of the project to lookup path for + :param project_path: path to the project to add to .setreg file + :param force: if set, the project path will be set within the .setreg file regardless of if the path doesn't exist :return: 0 for success or non 0 failure code """ - if project_path and project_name: - logger.error(f'Project Name and Project Path provided, these are mutually exclusive.') - return 1 - + # we need either a project name or path if not project_name and not project_path: - logger.error('Must specify either a Project name or Project Path.') + logger.error(f'Must either specify a Project path or Project Name.') return 1 + # if project name resolve it into a path if project_name and not project_path: project_path = manifest.get_registered(project_name=project_name) if not project_path: - logger.error(f'Project Path {project_path} has not been registered.') + logger.error( + f'The project name has been supplied. Unable to locate project path from the registered manifest.json files:' + f' {str(pathlib.Path("~/.o3de/o3de_manifest.json").expanduser())}, engine.json\n' + 'A The --project-path parameter can be used directly to skip checking the manifest') return 1 - project_path = pathlib.Path(project_path).resolve() + # Only perform project path validations when force=False + if not force: + if not project_path.is_dir(): + logger.error(f'Project path {project_path} is not a folder.') + return 1 - bootstrap_setreg_file = manifest.get_o3de_registry_folder() / 'bootstrap.setreg' - if bootstrap_setreg_file.is_file(): - with bootstrap_setreg_file.open('r') as f: + # Validate that the supplied path points contains a valid project.json + if not validation.valid_o3de_project_json(project_path / 'project.json'): + logger.error(f'The supplied project path does not contain a valid project.json.\n' + f'The Path will not be set') + return 1 + + # If the output_path exist validate that it is a valid json file and read it's json data + setreg_json_data = get_json_data(output_path) + if output_path.is_file(): + with output_path.open('r') as f: try: - json_data = json.load(f) - except json.JSONDecodeError as e: - logger.error(f'Bootstrap.setreg failed to load: {str(e)}') - else: - try: - json_data["Amazon"]["AzCore"]["Bootstrap"]["project_path"] = project_path - except KeyError as e: - logger.error(f'Bootstrap.setreg failed to load: {str(e)}') - else: - try: - os.unlink(bootstrap_setreg_file) - except OSError as e: - logger.error(f'Failed to unlink bootstrap file {bootstrap_setreg_file}: {str(e)}') - return 1 - else: - json_data = {} - json_data.update({"Amazon":{"AzCore":{"Bootstrap":{"project_path":project_path.as_posix()}}}}) + setreg_json_data = json.load(f) + except (json.JSONDecodeError) as e: + logger.error(f'The output file: {output_path} is not a valid json file: {str(e)}') + return 1 - with bootstrap_setreg_file.open('w') as s: - s.write(json.dumps(json_data, indent=4)) + # Add a json dictionary that will be merged with any existing json data from the .setreg file + merge_json_data = {} + json_object_iter = merge_json_data + for json_key in PROJECT_PATH_KEY[:-1]: + # Add the parent json object for the key to update + json_object_iter = json_object_iter.setdefault(json_key, {}) + + # Set the project path value here + json_object_iter[PROJECT_PATH_KEY[-1]] = project_path.as_posix() + setreg_json_data.update(merge_json_data) + + # Create the parent directories + if output_path.parent: + output_path.parent.mkdir(parents=True, exist_ok=True) + try: + with output_path.open('w') as s: + s.write(json.dumps(setreg_json_data, indent=4) + '\n') + except OSError as e: + logger.error(f'Failed to write project path {project_path} to file {output_path}: {str(e)}') + return 1 return 0 -def get_global_project() -> pathlib.Path or None: +def get_global_project(input_path: pathlib.Path) -> pathlib.Path or None: """ - get what the current project set is + Retrieves the /Amazon/AzCore/Bootstrap/project_path key from the supplied file path :return: project_path or None on failure """ - bootstrap_setreg_file = manifest.get_o3de_registry_folder() / 'bootstrap.setreg' - if not bootstrap_setreg_file.is_file(): - logger.error(f'Bootstrap.setreg file {bootstrap_setreg_file} does not exist.') - return None + setreg_json_data = get_json_data(input_path) - with bootstrap_setreg_file.open('r') as f: - try: - json_data = json.load(f) - except json.JSONDecodeError as e: - logger.error(f'Bootstrap.setreg failed to load: {str(e)}') - else: - try: - project_path = json_data["Amazon"]["AzCore"]["Bootstrap"]["project_path"] - except KeyError as e: - logger.error(f'Bootstrap.setreg cannot find Amazon:AzCore:Bootstrap:project_path: {str(e)}') - else: - return pathlib.Path(project_path).resolve() + try: + # Iterate over each element of the tuple and read the json key from each successive json object + json_object_iter = setreg_json_data + for json_key in PROJECT_PATH_KEY: + json_object_iter = json_object_iter[json_key] + except KeyError as e: + logger.error(f'Cannot read key /{"/".join(PROJECT_PATH_KEY)} from file {input_path.as_posix()}: {str(e)}') + else: + project_path = json_object_iter + return pathlib.Path(project_path).resolve() return None def _run_get_global_project(args: argparse) -> int: - if args.override_home_folder: - manifest.override_home_folder = args.override_home_folder - - project_path = get_global_project() + project_path = get_global_project(args.input_path) if project_path: print(project_path.as_posix()) return 0 @@ -111,51 +139,66 @@ def _run_get_global_project(args: argparse) -> int: def _run_set_global_project(args: argparse) -> int: - if args.override_home_folder: - manifest.override_home_folder = args.override_home_folder + return set_global_project(args.output_path, + args.project_name, + args.project_path, + args.force) - return set_global_project(args.project_name, - args.project_path) +def add_parser_args(get_project_parser, set_project_parser): + """ + add_parser_args is called to add arguments to each command such that it can be + invoked locally or added by a central python file. + Ex. Directly run from this file alone with: python global_project.py --project-path "D:/TestProject" + :param parser: the caller passes an argparse parser like instance to this method + """ + + # get-current-project + get_project_parser.add_argument('-i', '--input-path', type=pathlib.Path, required=False, default=DEFAULT_BOOTSTRAP_SETREG, + help=f'Optional path to file to read /{"/".join(PROJECT_PATH_KEY)} key from.' + f' If not supplied, then {DEFAULT_BOOTSTRAP_SETREG} is used instead') + get_project_parser.set_defaults(func=_run_get_global_project) + + # set-current-project + group = set_project_parser.add_mutually_exclusive_group(required=True) + group.add_argument('-pp', '--project-path', type=pathlib.Path, required=False, + help='The path to the project.') + group.add_argument('-pn', '--project-name', type=str, required=False, + help='The name of the project.') + set_project_parser.add_argument('-o', '--output-path', type=pathlib.Path, required=False, + default=DEFAULT_BOOTSTRAP_SETREG, + help=f'Optional path to output file to write project_path key to. ' + f'If not supplied, then {DEFAULT_BOOTSTRAP_SETREG} is used instead') + set_project_parser.add_argument('-f', '--force', action='store_true', default=False, + help=f'Force the setting of the project path in the supplied setreg file') + set_project_parser.set_defaults(func=_run_set_global_project) 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: python global_project.py set_global_project --project-name TestProject - OR - o3de.py can aggregate commands by importing global_project, call add_args and - execute: python o3de.py set_global_project --project-path C:/TestProject + add_args is called to add subparsers arguments to each command such that it can be + a central python file such as o3de.py. + It can be run from the o3de.py script as follows + call add_args and execute: python o3de.py set-global-project --project-path "D:/TestProject" :param subparsers: the caller instantiates subparsers and passes it in here """ - get_global_project_subparser = subparsers.add_parser('get-global-project') - get_global_project_subparser.add_argument('-ohf', '--override-home-folder', type=str, required=False, - help='By default the home folder is the user folder, override it to this folder.') - - get_global_project_subparser.set_defaults(func=_run_get_global_project) - - set_global_project_subparser = subparsers.add_parser('set-global-project') - group = set_global_project_subparser.add_mutually_exclusive_group(required=True) - group.add_argument('-pn', '--project-name', required=False, - help='The name of the project. If supplied this will resolve the --project-path.') - group.add_argument('-pp', '--project-path', required=False, - help='The path to the project') - - set_global_project_subparser.add_argument('-ohf', '--override-home-folder', type=str, required=False, - help='By default the home folder is the user folder, override it to this folder.') - - set_global_project_subparser.set_defaults(func=_run_set_global_project) + get_project_subparser = subparsers.add_parser('get-global-project') + set_project_subparser = subparsers.add_parser('set-global-project') + add_parser_args(get_project_subparser, set_project_subparser) -if __name__ == "__main__": +def main(): + """ + Runs this script as standalone script + """ # parse the command line args the_parser = argparse.ArgumentParser() # add subparsers - the_subparsers = the_parser.add_subparsers(help='sub-command help', dest='command', required=True) + project_subparsers = the_parser.add_subparsers(help="Commands for modifying the project path in the user's home" + " setreg files") # add args to the parser - add_args(the_subparsers) + add_args(project_subparsers) # parse args the_args = the_parser.parse_args() @@ -165,3 +208,7 @@ if __name__ == "__main__": # return sys.exit(ret) + + +if __name__ == "__main__": + main() diff --git a/scripts/o3de/o3de/manifest.py b/scripts/o3de/o3de/manifest.py index 092430500f..9436e1dd29 100644 --- a/scripts/o3de/o3de/manifest.py +++ b/scripts/o3de/o3de/manifest.py @@ -142,7 +142,7 @@ def get_o3de_manifest() -> pathlib.Path: with default_restricted_folder_json.open('w') as s: restricted_json_data = {} restricted_json_data.update({'restricted_name': 'o3de'}) - s.write(json.dumps(restricted_json_data, indent=4)) + s.write(json.dumps(restricted_json_data, indent=4) + '\n') json_data.update({'default_restricted_folder': default_restricted_folder.as_posix()}) default_projects_restricted_folder_json = default_projects_restricted_folder / 'restricted.json' @@ -150,24 +150,24 @@ def get_o3de_manifest() -> pathlib.Path: with default_projects_restricted_folder_json.open('w') as s: restricted_json_data = {} restricted_json_data.update({'restricted_name': 'projects'}) - s.write(json.dumps(restricted_json_data, indent=4)) + s.write(json.dumps(restricted_json_data, indent=4) + '\n') default_gems_restricted_folder_json = default_gems_restricted_folder / 'restricted.json' if not default_gems_restricted_folder_json.is_file(): with default_gems_restricted_folder_json.open('w') as s: restricted_json_data = {} restricted_json_data.update({'restricted_name': 'gems'}) - s.write(json.dumps(restricted_json_data, indent=4)) + s.write(json.dumps(restricted_json_data, indent=4) + '\n') default_templates_restricted_folder_json = default_templates_restricted_folder / 'restricted.json' if not default_templates_restricted_folder_json.is_file(): with default_templates_restricted_folder_json.open('w') as s: restricted_json_data = {} restricted_json_data.update({'restricted_name': 'templates'}) - s.write(json.dumps(restricted_json_data, indent=4)) + s.write(json.dumps(restricted_json_data, indent=4) + '\n') with manifest_path.open('w') as s: - s.write(json.dumps(json_data, indent=4)) + s.write(json.dumps(json_data, indent=4) + '\n') return manifest_path @@ -201,7 +201,7 @@ def save_o3de_manifest(json_data: dict, manifest_path: pathlib.Path = None) -> N manifest_path = get_o3de_manifest() with manifest_path.open('w') as s: try: - s.write(json.dumps(json_data, indent=4)) + s.write(json.dumps(json_data, indent=4) + '\n') except OSError as e: logger.error(f'Manifest json failed to save: {str(e)}') diff --git a/scripts/o3de/o3de/register.py b/scripts/o3de/o3de/register.py index 1b30448db9..68575488dc 100644 --- a/scripts/o3de/o3de/register.py +++ b/scripts/o3de/o3de/register.py @@ -410,12 +410,8 @@ def register_project_path(json_data: dict, if update_project_json: project_json_data['engine'] = this_engine_json['engine_name'] utils.backup_file(project_json) - with project_json.open('w') as s: - try: - s.write(json.dumps(project_json_data, indent=4)) - except OSError as e: - logger.error(f'Project json failed to save: {str(e)}') - return 1 + if not manifest.save_o3de_manifest(project_json_data, project_path): + return 1 return 0 diff --git a/scripts/o3de/o3de/sha256.py b/scripts/o3de/o3de/sha256.py index bbec7696d6..db0a1fe834 100644 --- a/scripts/o3de/o3de/sha256.py +++ b/scripts/o3de/o3de/sha256.py @@ -51,7 +51,7 @@ def sha256(file_path: str or pathlib.Path, utils.backup_file(json_path) with json_path.open('w') as s: try: - s.write(json.dumps(json_data, indent=4)) + s.write(json.dumps(json_data, indent=4) + '\n') except OSError as e: logger.error(f'Failed to write Json path {json_path}: {str(e)}') return 1 diff --git a/scripts/o3de/tests/CMakeLists.txt b/scripts/o3de/tests/CMakeLists.txt index 5c83f4112b..0526c7740d 100644 --- a/scripts/o3de/tests/CMakeLists.txt +++ b/scripts/o3de/tests/CMakeLists.txt @@ -27,3 +27,10 @@ ly_add_pytest( TEST_SUITE smoke EXCLUDE_TEST_RUN_TARGET_FROM_IDE ) + +ly_add_pytest( + NAME o3de_global_project + PATH ${CMAKE_CURRENT_LIST_DIR}/unit_test_global_project.py + TEST_SUITE smoke + EXCLUDE_TEST_RUN_TARGET_FROM_IDE +) diff --git a/scripts/o3de/tests/unit_global_project.py b/scripts/o3de/tests/unit_global_project.py new file mode 100644 index 0000000000..1d3a4dd4f4 --- /dev/null +++ b/scripts/o3de/tests/unit_global_project.py @@ -0,0 +1,40 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# + +import io +import json +import logging +import pytest +import pathlib +from unittest.mock import patch + +from o3de import global_project + + +logger = logging.getLogger() +logging.basicConfig() + +DEFAULT_BOOTSTRAP_SETREG = pathlib.Path('~/.o3de/Registry/bootstrap.setreg').expanduser() +PROJECT_PATH_KEY = ('Amazon', 'AzCore', 'Bootstrap', 'project_path') + +class TestSetGlobalProject: + @pytest.mark.parametrize( + "output_path, project_path, force, expected_result", [ + pytest.param(pathlib.Path('~/.o3de/Registry/bootstrap.setreg'), pathlib.Path('A:/'), False, False), + pytest.param(pathlib.Path('~/.o3de/Registry/bootstrap.setreg'), pathlib.Path('A:/'), True, True) + ] + ) + def test_set_global_project_non_existent_project_path(self, output_path, project_path, force, expected_result): + with patch('pathlib.Path.open', return_value=io.StringIO()) as pathlib_open_mock: + result = global_project.set_global_project(output_path, project_path=project_path, force=force) == 0 + + + assert result == expected_result diff --git a/scripts/o3de/tests/unit_test_current_project.py b/scripts/o3de/tests/unit_test_current_project.py deleted file mode 100755 index 7db48b62aa..0000000000 --- a/scripts/o3de/tests/unit_test_current_project.py +++ /dev/null @@ -1,102 +0,0 @@ -# -# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or -# its licensors. -# -# For complete copyright and license terms please see the LICENSE at the root of this -# distribution (the "License"). All use of this software is governed by the License, -# or, if provided, by the license below or the license accompanying this file. Do not -# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# - -import os -import pytest - -from . import current_project - -TEST_BOOTSTRAP_CONTENT_1 = """ -project_path = Game1 -foo = bar -key1 = value1 -key2 = value2 -assets = pc -""" -TEST_BOOTSTRAP_CONTENT_2 = """ -project_path=Game1 -foo = bar -key1 = value1 -key2 = value2 -assets = pc -""" -TEST_BOOTSTRAP_CONTENT_3 = """ -project_path= Game1 -foo = bar -key1 = value1 -key2 = value2 -assets = pc -""" -TEST_BOOTSTRAP_CONTENT_4 = """ -project_path =Game1 -foo = bar -key1 = value1 -key2 = value2 -assets = pc -""" -TEST_BOOTSTRAP_CONTENT_5 = """ -project_path = Game1 -foo = bar -key1 = value1 -key2 = value2 -assets = pc -""" - -@pytest.mark.parametrize( - "contents, expected_result", [ - pytest.param(TEST_BOOTSTRAP_CONTENT_1, 'Game1'), - pytest.param(TEST_BOOTSTRAP_CONTENT_2, 'Game1'), - pytest.param(TEST_BOOTSTRAP_CONTENT_3, 'Game1'), - pytest.param(TEST_BOOTSTRAP_CONTENT_4, 'Game1'), - pytest.param(TEST_BOOTSTRAP_CONTENT_5, 'Game1'), - ] -) -def test_get_current_project(tmpdir, contents, expected_result): - dev_root = str(tmpdir.join('dev').realpath()).replace('\\', '/') - os.makedirs(dev_root, exist_ok=True) - - bootstrap_file = f'{dev_root}/bootstrap.cfg' - if os.path.isfile(bootstrap_file): - os.unlink(bootstrap_file) - with open(bootstrap_file, 'a') as s: - s.write(contents) - - result = current_project.get_current_project(dev_root) - assert expected_result == result - - -@pytest.mark.parametrize( - "contents, project_to_set, expected_result", [ - pytest.param(TEST_BOOTSTRAP_CONTENT_1, 'Test1', 0), - pytest.param(TEST_BOOTSTRAP_CONTENT_1, ' Test2', 0), - pytest.param(TEST_BOOTSTRAP_CONTENT_1, 'Test3 ', 0), - pytest.param(TEST_BOOTSTRAP_CONTENT_1, '/Test4', 1), - pytest.param(TEST_BOOTSTRAP_CONTENT_1, '=Test5', 1), - ] -) -def test_set_current_project(tmpdir, contents, project_to_set, expected_result): - dev_root = str(tmpdir.join('dev').realpath()).replace('\\', '/') - os.makedirs(dev_root, exist_ok=True) - - bootstrap_file = f'{dev_root}/bootstrap.cfg' - if os.path.isfile(bootstrap_file): - os.unlink(bootstrap_file) - with open(bootstrap_file, 'a') as s: - s.write(contents) - - result = current_project.set_current_project(dev_root, project_to_set) - assert expected_result == result - - if result == 0: - project_that_is_set = current_project.get_current_project(dev_root) - print(project_that_is_set) - print(project_to_set) - assert project_to_set.strip() == project_that_is_set \ No newline at end of file