Merge branch 'main' into LoadPipelineFromGitHub

This commit is contained in:
Brian Herrera
2021-03-26 17:05:57 -07:00
765 changed files with 11513 additions and 20246 deletions
@@ -96,7 +96,7 @@ class AndroidDeployment(object):
if asset_mode == 'PAK':
self.local_asset_path = self.dev_root / 'Pak' / f'{game_name.lower()}_{asset_type}_paks'
else:
self.local_asset_path = self.dev_root / 'Cache' / game_name / asset_type
self.local_asset_path = self.dev_root / game_name / 'Cache' / asset_type
assert game_name is not None, f"'game_name' is required"
self.game_name = game_name
+77 -81
View File
@@ -84,7 +84,7 @@ APP_SPLASH_NAME = 'app_splash.png'
PYTHON_SCRIPT = 'python.cmd' if platform.system() == 'Windows' else 'python.sh'
ANDROID_LAUNCHER_NAME_PATTERN = "{game_name}.GameLauncher"
ANDROID_LAUNCHER_NAME_PATTERN = "{project_name}.GameLauncher"
class AndroidProjectManifestEnvironment(object):
"""
@@ -92,43 +92,42 @@ class AndroidProjectManifestEnvironment(object):
that were passed in or calculated from the command line arguments.
"""
def __init__(self, dev_root, game_name, android_sdk_version_number, android_ndk_platform_number):
def __init__(self, engine_root, project_path, android_sdk_version_number, android_ndk_platform_number, is_test:bool):
"""
Initialize the object with the project specific parameters and values for the game project
:param dev_root: The dev-root path where the game is located
:param game_name: The name of the game
:param engine_root: The path where the engine is located
:param project_path: The path were the project is located
:param android_sdk_version_number: The android SDK platform version
:param android_ndk_platform_number: The android NDK platform version
:param is_test: Indicates if theAzTestRunner application should be run
"""
is_test = game_name.lower() == TEST_RUNNER_PROJECT.lower()
if is_test:
# The AzTestRunner project.json is located under {dev_root}/Code/Tools/AzTestRunner/Platform/Android/android_project.json
game_folder_project_properties_path = dev_root / 'Code' / 'Tools' / 'AzTestRunner' / 'Platform' / 'Android' / 'android_project.json'
# The AzTestRunner project.json is located under {engine_root}/Code/Tools/AzTestRunner/Platform/Android/android_project.json
project_properties_path = engine_root / 'Code' / 'Tools' / 'AzTestRunner' / 'Platform' / 'Android' / 'android_project.json'
else:
# The project.json file is located under the game name folder
game_folder = dev_root / game_name
game_folder_project_properties_path = game_folder / 'project.json'
project_properties_path = project_path / 'project.json'
# Read and parse the project.json file into a dictionary to process the specific attributes needed for the manifest template
game_project_properties_content = game_folder_project_properties_path.resolve(strict=True)\
project_properties_content = project_properties_path.resolve(strict=True)\
.read_text(encoding=common.DEFAULT_TEXT_READ_ENCODING,
errors=common.ENCODING_ERROR_HANDLINGS)
self.game_name = game_name
self.project_path = project_path
# Extract the key attributes we need to process and build up our environment table
game_project_json = json.loads(game_project_properties_content)
project_json = json.loads(project_properties_content)
product_name = game_project_json['product_name']
project_name = project_json['project_name']
product_name = project_json['product_name']
game_project_android_settings = game_project_json['android_settings']
game_project_android_settings = project_json['android_settings']
package_name = game_project_android_settings["package_name"]
package_path = package_name.replace('.', '/')
project_activity = f'{TEST_RUNNER_PROJECT}Activity' if is_test else f'{self.game_name}Activity'
project_activity = f'{TEST_RUNNER_PROJECT}Activity' if is_test else f'{project_name}Activity'
# Multiview options require special processing
multi_window_options = AndroidProjectManifestEnvironment.process_android_multi_window_options(game_project_android_settings)
@@ -140,9 +139,9 @@ class AndroidProjectManifestEnvironment(object):
"ANDROID_VERSION_NAME": game_project_android_settings["version_name"],
"ANDROID_SCREEN_ORIENTATION": game_project_android_settings["orientation"],
'ANDROID_APP_NAME': TEST_RUNNER_PROJECT if is_test else product_name, # external facing name
'ANDROID_PROJECT_NAME': TEST_RUNNER_PROJECT if is_test else self.game_name, # internal facing name
'ANDROID_PROJECT_NAME': TEST_RUNNER_PROJECT if is_test else project_name, # internal facing name
'ANDROID_PROJECT_ACTIVITY': project_activity,
'ANDROID_LAUNCHER_NAME': TEST_RUNNER_PROJECT if is_test else ANDROID_LAUNCHER_NAME_PATTERN.format(game_name=self.game_name),
'ANDROID_LAUNCHER_NAME': TEST_RUNNER_PROJECT if is_test else ANDROID_LAUNCHER_NAME_PATTERN.format(project_name=project_name),
'ANDROID_CONFIG_CHANGES': multi_window_options['ANDROID_CONFIG_CHANGES'],
'ANDROID_APP_PUBLIC_KEY': game_project_android_settings.get('app_public_key', 'NoKey'),
'ANDROID_APP_OBFUSCATOR_SALT': game_project_android_settings.get('app_obfuscator_salt', ''),
@@ -297,7 +296,7 @@ PLATFORM_SETTINGS_FORMAT = """
[settings]
platform={platform}
game_projects={game_project}
game_projects={project_path}
asset_deploy_mode={asset_mode}
asset_deploy_type={asset_type}
@@ -336,7 +335,7 @@ CUSTOM_GRADLE_COPY_NATIVE_CONFIG_FORMAT_STR = """
task copyNativeLibs{config}(type: Copy) {{
delete 'outputs/native-lib/{abi}'
from fileTree(dir: 'build/intermediates/cmake/{config_lower}/obj/arm64-v8a/{config_lower}', include: '**/*.so', exclude: 'lib{game_name}.GameLauncher.so' )
from fileTree(dir: 'build/intermediates/cmake/{config_lower}/obj/arm64-v8a/{config_lower}', include: '**/*.so', exclude: 'lib{project_name}.GameLauncher.so' )
into 'outputs/native-lib/{abi}'
}}
@@ -363,7 +362,7 @@ CUSTOM_GRADLE_COPY_NATIVE_CONFIG_BUILD_ARTIFACTS_FORMAT_STR = """
CUSTOM_APPLY_ASSET_LAYOUT_TASK_FORMAT_STR = """
task syncLYLayoutMode{config}(type:Exec) {{
workingDir '{working_dir}'
commandLine '{python_full_path}', 'layout_tool.py', '--dev-root', '{dev_root}', '-p', 'Android', '-a', '{asset_type}', '-g', '{game_name}', '-m', '{asset_mode}', '--create-layout-root', '-l', '{asset_layout_folder}'
commandLine '{python_full_path}', 'layout_tool.py', '--project-path', '{project_path}', '-p', 'Android', '-a', '{asset_type}', '-m', '{asset_mode}', '--create-layout-root', '-l', '{asset_layout_folder}'
}}
compile{config}Sources.dependsOn syncLYLayoutMode{config}
"""
@@ -424,17 +423,19 @@ class AndroidProjectGenerator(object):
Class the manages the process to generate an android project folder in order to build with gradle/android studio
"""
def __init__(self, dev_root, build_dir, android_ndk_path, android_sdk_path, android_sdk_version, android_ndk_platform, game_name, third_party_path, cmake_version, override_cmake_path, override_gradle_path, override_ninja_path, android_sdk_build_tool_version, include_assets_in_apk, asset_mode, asset_type, signing_config, is_test_project=False):
def __init__(self, engine_root, build_dir, android_ndk_path, android_sdk_path, android_sdk_version, android_ndk_platform,
project_path, third_party_path, cmake_version, override_cmake_path, override_gradle_path, override_ninja_path,
android_sdk_build_tool_version, include_assets_in_apk, asset_mode, asset_type, signing_config, is_test_project=False):
"""
Initialize the object with all the required parameters needed to create an Android Project. The parameters should be verified before initializing this object
:param dev_root: The dev-root that contains the engine and the game
:param build_dir: The target folder under the ${dev_root} where the android project folder will be created
:param engine_root: The engine root that contains the engine
:param build_dir: The target folder under the where the android project folder will be created
:param android_ndk_path: The path to the ANDROID_NDK used for building the native android code
:param android_sdk_path: The path to the ANDROID_SDK used for building the android java code
:param android_sdk_version: The android platform version number to use for the Android SDK related builds
:param android_ndk_platform: The android platform version number to use for the Android NDK related builds
:param game_name: The name of the game under the ${dev_root} to create the project for
:param project_path: The path to the project
:param third_party_path: The required path to the lumberyard 3rd party path
:param cmake_version: The version number of cmake that will be used by gradle
:param override_cmake_path: The override path to cmake if it does not exists in the system path
@@ -445,11 +446,11 @@ class AndroidProjectGenerator(object):
:param asset_mode:
:param asset_type:
:param signing_config: Optional signing configuration arguments
:param is_test_project: Flag to indicate if this is a unit test runner project. (If true, game_name, asset_mode, asset_type, and include_assets_in_apk are ignored)
:param is_test_project: Flag to indicate if this is a unit test runner project. (If true, project_path, asset_mode, asset_type, and include_assets_in_apk are ignored)
"""
self.env = {}
self.dev_root = dev_root
self.engine_root = engine_root
self.build_dir = build_dir
@@ -457,7 +458,7 @@ class AndroidProjectGenerator(object):
self.android_sdk_path = android_sdk_path
self.android_project_builder_path = dev_root / 'Code/Tools/Android/ProjectBuilder'
self.android_project_builder_path = self.engine_root / 'Code/Tools/Android/ProjectBuilder'
self.android_sdk_version = android_sdk_version
@@ -465,7 +466,7 @@ class AndroidProjectGenerator(object):
self.android_ndk_platform = android_ndk_platform
self.game_name = game_name
self.project_path = project_path
self.third_party_path = third_party_path
@@ -507,7 +508,7 @@ class AndroidProjectGenerator(object):
'SDK_VER': self.android_sdk_version,
'NDK_PLATFORM_VER': self.android_ndk_platform,
'SDK_BUILD_TOOL_VER': self.android_sdk_build_tool_version,
'LY_DEV_ROOT': common.normalize_path_for_settings(self.dev_root)
'LY_ENGINE_ROOT': common.normalize_path_for_settings(self.engine_root)
}
# Generate the gradle build script
self.create_file_from_project_template(src_template_file='root.build.gradle.in',
@@ -570,7 +571,7 @@ class AndroidProjectGenerator(object):
if self.is_test_project:
platform_settings_content = PLATFORM_SETTINGS_FORMAT.format(generation_timestamp=str(datetime.datetime.now().strftime("%c")),
platform='android',
game_project=TEST_RUNNER_PROJECT,
project_path=TEST_RUNNER_PROJECT,
asset_mode='',
asset_type='',
android_sdk_path=str(self.android_sdk_path),
@@ -578,7 +579,7 @@ class AndroidProjectGenerator(object):
else:
platform_settings_content = PLATFORM_SETTINGS_FORMAT.format(generation_timestamp=str(datetime.datetime.now().strftime("%c")),
platform='android',
game_project=self.game_name,
project_path=self.project_path,
asset_mode=self.asset_mode,
asset_type=self.asset_type,
android_sdk_path=str(self.android_sdk_path),
@@ -709,22 +710,16 @@ class AndroidProjectGenerator(object):
# Prepare the 'PROJECT_DEPENDENCIES' environment variable
gradle_project_dependencies = [f" api project(path: ':{project_dependency}')" for project_dependency in project_dependencies]
template_dev_root = common.normalize_path_for_settings(self.dev_root)
template_engine_root = common.normalize_path_for_settings(self.engine_root)
template_third_party_path = common.normalize_path_for_settings(self.third_party_path)
template_ndk_path = common.normalize_path_for_settings(self.android_ndk_path)
gradle_build_env = dict()
# Calculate the relative path of the engine root based on the target build directory, which is
# expected to be at the same folder level as the engine root
relative_engine_root_path = ''
target_app_root_path = self.dev_root / self.build_dir / 'app'
while target_app_root_path != self.dev_root:
target_app_root_path = target_app_root_path.parent
relative_engine_root_path += '../'
engine_root_as_path= pathlib.PurePath(self.engine_root)
relative_cmakelist_path = relative_engine_root_path + 'CMakeLists.txt'
relative_azandroid_path = relative_engine_root_path + 'Code/Framework/AzAndroid/java'
relative_cmakelist_path = (engine_root_as_path / 'CMakeLists.txt').as_posix()
relative_azandroid_path = (engine_root_as_path / 'Code/Framework/AzAndroid/java').as_posix()
gradle_build_env['TARGET_TYPE'] = 'application'
gradle_build_env['PROJECT_DEPENDENCIES'] = PROJECT_DEPENDENCIES_VALUE_FORMAT.format(dependencies='\n'.join(gradle_project_dependencies))
@@ -744,13 +739,13 @@ class AndroidProjectGenerator(object):
# Prepare the cmake argument list based on the collected android settings and each build config
cmake_argument_list = [
'"-GNinja"',
f'"-S{template_dev_root}"',
f'"-S{template_engine_root}"',
f'"-DCMAKE_BUILD_TYPE={native_config_lower}"',
f'"-DCMAKE_TOOLCHAIN_FILE={template_dev_root}/cmake/Platform/Android/Toolchain_Android.cmake"',
f'"-DCMAKE_TOOLCHAIN_FILE={template_engine_root}/cmake/Platform/Android/Toolchain_Android.cmake"',
f'"-DLY_3RDPARTY_PATH={template_third_party_path}"']
if not self.is_test_project:
cmake_argument_list.append(f'"-DLY_PROJECTS={self.game_name}"')
cmake_argument_list.append(f'"-DLY_PROJECTS={pathlib.PurePath(self.project_path).as_posix()}"')
else:
cmake_argument_list.append('"-DLY_TEST_PROJECT=1"')
@@ -766,44 +761,46 @@ class AndroidProjectGenerator(object):
if self.override_ninja_path:
cmake_argument_list.append(f'"-DCMAKE_MAKE_PROGRAM={common.normalize_path_for_settings(self.override_ninja_path)}"')
# Query the project_path from the project.json file
project_name = common.read_project_name_from_project_json(self.project_path)
# Prepare the config-specific section to place the cmake argument list in the build.gradle for the app
gradle_build_env[f'NATIVE_CMAKE_SECTION_{native_config_upper}_CONFIG'] = \
NATIVE_CMAKE_SECTION_BUILD_TYPE_CONFIG_FORMAT_STR.format(targets_section=f'targets "{self.game_name}.GameLauncher"' if not self.is_test_project else "",
arguments=','.join(cmake_argument_list))
NATIVE_CMAKE_SECTION_BUILD_TYPE_CONFIG_FORMAT_STR.format(targets_section=f'targets "{project_name}.GameLauncher"'
if project_name and not self.is_test_project else "", arguments=','.join(cmake_argument_list))
# Prepare the config-specific section to copy the related .so files that are marked as dependencies for the target
# (launcher) since gradle will not include them automatically for APK import
gradle_build_env[f'CUSTOM_GRADLE_COPY_NATIVE_{native_config_upper}_LIB_TASK'] = \
CUSTOM_GRADLE_COPY_NATIVE_CONFIG_FORMAT_STR.format(config=native_config,
config_lower=native_config_lower,
game_name=self.game_name,
abi=ANDROID_ARCH,
optional_test_excludes=",'*.Tests.so'" if not self.is_test_project else "")
if project_name:
# Prepare the config-specific section to copy the related .so files that are marked as dependencies for the target
# (launcher) since gradle will not include them automatically for APK import
gradle_build_env[f'CUSTOM_GRADLE_COPY_NATIVE_{native_config_upper}_LIB_TASK'] = \
CUSTOM_GRADLE_COPY_NATIVE_CONFIG_FORMAT_STR.format(config=native_config,
config_lower=native_config_lower,
project_name=project_name,
abi=ANDROID_ARCH,
optional_test_excludes=",'*.Tests.so'" if not self.is_test_project else "")
if self.is_test_project:
gradle_build_env[f'CUSTOM_APPLY_ASSET_LAYOUT_{native_config_upper}_TASK'] = \
CUSTOM_GRADLE_COPY_NATIVE_CONFIG_BUILD_ARTIFACTS_FORMAT_STR.format(config=native_config,
config_lower=native_config_lower,
asset_layout_folder=common.normalize_path_for_settings(self.dev_root / self.build_dir / 'app/src/main/assets'),
asset_layout_folder=(self.project_path / self.build_dir / 'app/src/main/assets').as_posix(),
file_includes='Test.Assets/**/*.*')
else:
# Copy over settings registry files from the Registry folder with build output directory
gradle_build_env[f'CUSTOM_APPLY_ASSET_LAYOUT_{native_config_upper}_TASK'] = \
CUSTOM_GRADLE_COPY_NATIVE_CONFIG_BUILD_ARTIFACTS_FORMAT_STR.format(config=native_config,
config_lower=native_config_lower,
asset_layout_folder=common.normalize_path_for_settings(self.dev_root / self.build_dir / 'app/src/main/assets'),
asset_layout_folder=(self.project_path / self.build_dir / 'app/src/main/assets').as_posix(),
file_includes='**/Registry/*.setreg')
if self.include_assets_in_apk:
if not self.is_test_project:
gradle_build_env[f'CUSTOM_APPLY_ASSET_LAYOUT_{native_config_upper}_TASK'] += \
CUSTOM_APPLY_ASSET_LAYOUT_TASK_FORMAT_STR.format(working_dir=common.normalize_path_for_settings(self.dev_root / 'cmake/Tools'),
python_full_path=common.normalize_path_for_settings(self.dev_root / 'python' / PYTHON_SCRIPT),
dev_root=common.normalize_path_for_settings(self.dev_root),
CUSTOM_APPLY_ASSET_LAYOUT_TASK_FORMAT_STR.format(working_dir=common.normalize_path_for_settings(self.engine_root / 'cmake/Tools'),
python_full_path=common.normalize_path_for_settings(self.engine_root / 'python' / PYTHON_SCRIPT),
asset_type=self.asset_type,
game_name=self.game_name,
project_path=self.project_path.as_posix(),
asset_mode=self.asset_mode if native_config != 'Release' else 'PAK',
asset_layout_folder=common.normalize_path_for_settings(self.dev_root / self.build_dir / 'app/src/main/assets'),
asset_layout_folder=common.normalize_path_for_settings(self.project_path / self.build_dir / 'app/src/main/assets'),
config=native_config)
else:
gradle_build_env[f'CUSTOM_APPLY_ASSET_LAYOUT_{native_config_upper}_TASK'] = ''
@@ -833,10 +830,11 @@ class AndroidProjectGenerator(object):
# Generate a AndroidManifest.xml and write to ${az_android_dst_path}/src/main/AndroidManifest.xml
dest_src_main_path = az_android_dst_path / 'src/main'
dest_src_main_path.mkdir(parents=True)
az_android_package_env = AndroidProjectManifestEnvironment(dev_root=self.dev_root,
game_name=self.game_name,
az_android_package_env = AndroidProjectManifestEnvironment(engine_root=self.engine_root,
project_path=self.project_path,
android_sdk_version_number=self.android_sdk_version,
android_ndk_platform_number=self.android_ndk_platform)
android_ndk_platform_number=self.android_ndk_platform,
is_test=self.is_test_project)
self.create_file_from_project_template(src_template_file=ANDROID_MANIFEST_FILE,
template_env=az_android_package_env,
dst_file=dest_src_main_path / ANDROID_MANIFEST_FILE,
@@ -972,17 +970,12 @@ class AndroidProjectGenerator(object):
# Always return itself if the path is already and absolute path
return pathlib.Path(source_path)
game_gem_resources = self.dev_root / self.game_name / 'Gem' / 'Resources'
game_gem_resources = self.project_path / 'Gem' / 'Resources'
if game_gem_resources.is_dir(game_gem_resources):
# If the source is relative and the game gem's resource is present, construct the path based on that
return game_gem_resources / source_path
legacy_game_resources = self.dev_root / 'Code' / self.game_name / 'Resources'
if legacy_game_resources.is_dir(game_gem_resources):
# If the source is relative and the game is using the legacy code folder's resource, construct the path based on that
return legacy_game_resources / source_path
raise common.LmbrCmdError("Unable to locate resources folder for game '{}'".format(self.game_name))
raise common.LmbrCmdError(f'Unable to locate resources folder for project at path "{self.project_path}"')
def resolve_icon_overrides(self, az_android_dst_path, az_android_package_env):
"""
@@ -1012,7 +1005,7 @@ class AndroidProjectGenerator(object):
shutil.copyfile(src_default_icon_file.resolve(), dst_default_icon_file.resolve())
os.chmod(dst_default_icon_file.resolve(), stat.S_IWRITE | stat.S_IREAD)
else:
logging.debug('No default icon override specified for %s', self.game_name)
logging.debug(f'No default icon override specified for project_at path {self.project_path}')
# process each of the resolution overrides
warnings = []
@@ -1028,11 +1021,11 @@ class AndroidProjectGenerator(object):
# if both the resolution and the default are unspecified, warn the user but do nothing
if icon_source is None:
warnings.append(f'No icon override found for "{resolution}". Either supply one for "{resolution}" or a '
f'"default" in the android_settings "icon" section of the project.json file for {self.game_name}')
f'"default" in the android_settings "icon" section of the project.json file for {self.project_path}')
# if only the resolution is unspecified, remove the resolution specific version from the project
else:
logging.debug('Default icon being used for "%s" in %s', resolution, self.game_name)
logging.debug(f'Default icon being used for "{resolution}" in {self.project_path}', resolution)
common.remove_dir_path(target_directory)
continue
@@ -1072,7 +1065,7 @@ class AndroidProjectGenerator(object):
unused_override_warning = None
if (orientation & orientation_flag) == 0:
unused_override_warning = f'Splash screen overrides specified for "{orientation_key}" when desired orientation ' \
f'is set to "{ORIENTATION_FLAG_TO_KEY_MAP[orientation]}" in project {self.game_name}. ' \
f'is set to "{ORIENTATION_FLAG_TO_KEY_MAP[orientation]}" in project {self.project_path}. ' \
f'These overrides will be ignored.'
# if a default splash image is specified for this orientation, then copy it into the generic drawable-<orientation> folder
@@ -1092,7 +1085,8 @@ class AndroidProjectGenerator(object):
shutil.copyfile(src_default_splash_img_file.resolve(), dst_default_splash_img_file.resolve())
os.chmod(dst_default_splash_img_file.resolve(), stat.S_IWRITE | stat.S_IREAD)
else:
logging.debug(f'No default splash screen override specified for "%s" orientation in %s', orientation_key, self.game_name)
logging.debug(f'No default splash screen override specified for "%s" orientation in %s', orientation_key,
self.project_path)
# process each of the resolution overrides
warnings = []
@@ -1113,10 +1107,10 @@ class AndroidProjectGenerator(object):
section = f"{orientation_key}-{resolution}"
warnings.append(f'No splash screen override found for "{section}". Either supply one for "{resolution}" '
f'or a "default" in the android_settings "splash_screen-{orientation_key}" section of the '
f'project.json file for {self.game_name}.')
f'project.json file for {self.project_path}.')
else:
# if only the resolution is unspecified, remove the resolution specific version from the project
logging.debug('Default splash screen being used for "%s-%s" in %s', orientation_key, resolution, self.game_name)
logging.debug(f'Default splash screen being used for "{orientation_key}-{resolution}" in {self.project_path}')
common.remove_dir_path(target_directory)
continue
src_splash_img_file = self.construct_source_resource_path(splash_img_source)
@@ -1133,7 +1127,8 @@ class AndroidProjectGenerator(object):
for warning_msg in warnings:
logging.warning(warning_msg)
def clear_unused_assets(self, az_android_dst_path, az_android_package_env):
@staticmethod
def clear_unused_assets(az_android_dst_path, az_android_package_env):
"""
micro-optimization to clear assets from the final bundle that won't be used
@@ -1208,7 +1203,7 @@ class AndroidProjectGenerator(object):
else:
project_dependencies = ""
# Prepare an environemt for a basic, no-native (cmake) gradle project (java only)
# Prepare an environment for a basic, no-native (cmake) gradle project (java only)
build_gradle_env = {
'PROJECT_DEPENDENCIES': project_dependencies,
'TARGET_TYPE': 'library',
@@ -1478,6 +1473,7 @@ def verify_android_ndk(android_ndk_platform, argument_name, override_android_ndk
validated_android_platforms.append(dir_item.name)
# For NDK revisions 19 and up, there is a mapping file for version numbers that map to other version.
platforms_map_aliases = {}
if ndk_revision_number >= LooseVersion('19.0.0'):
platforms_map_file = check_android_ndk_path / 'meta/platforms.json'
if platforms_map_file.exists():
@@ -26,7 +26,6 @@ if ROOT_DEV_PATH not in sys.path:
from cmake.Tools import common
from cmake.Tools.Platform.Android import android_support
GRADLE_ARGUMENT_NAME = '--gradle-install-path'
GRADLE_MIN_VERSION = LooseVersion('4.10.1')
GRADLE_MAX_VERSION = LooseVersion('5.6.4')
@@ -148,12 +147,13 @@ def main(args):
parser = argparse.ArgumentParser(description="Prepare the android studio subfolder")
parser.add_argument('--dev-root',
help='The path to the dev root. Defaults to the current working directory.',
parser.add_argument('--engine-root',
help='The path to the engine root. Defaults to the current working directory.',
default=os.getcwd())
parser.add_argument('--build-dir',
help='The build dir subpath from the dev root',
help='The build dir path. It will be concatenated to the project-path using the rules of os.path.join',
type=pathlib.Path,
required=True)
parser.add_argument('--third-party-path',
@@ -196,8 +196,8 @@ def main(args):
default=None,
required=False)
parser.add_argument('-g', '--game-name',
help='The game project to base off of')
parser.add_argument('-g', '--project-path',
help='The project path to generate an android project')
# Asset Options
parser.add_argument(INCLUDE_APK_ASSETS_ARGUMENT_NAME,
@@ -266,15 +266,15 @@ def main(args):
verified_android_ndk_platform, verified_android_ndk_path = android_support.verify_android_ndk(android_ndk_platform=parsed_args.get_argument(ANDROID_NDK_PLATFORM_ARGUMENT_NAME),
argument_name=ANDROID_NDK_ARGUMENT_NAME,
override_android_ndk_path=parsed_args.get_argument(ANDROID_NDK_ARGUMENT_NAME))
if parsed_args.unit_test or parsed_args.game_name == android_support.TEST_RUNNER_PROJECT:
verified_game_name = android_support.TEST_RUNNER_PROJECT
_, verified_dev_root = common.verify_game_project_and_dev_root(game_name=None,
dev_root=parsed_args.dev_root)
if parsed_args.unit_test or parsed_args.project_path == android_support.TEST_RUNNER_PROJECT:
verified_project_path = android_support.TEST_RUNNER_PROJECT
_, verified_engine_root = common.verify_project_and_engine_root(project_root=None,
engine_root=parsed_args.dev_root)
is_test_project = True
else:
# Verify the dev-root and game name
verified_game_name, verified_dev_root = common.verify_game_project_and_dev_root(game_name=parsed_args.game_name,
dev_root=parsed_args.dev_root)
# Verify the engine root path and project path
verified_project_path, verified_engine_root = common.verify_project_and_engine_root(project_root=parsed_args.project_path,
engine_root=parsed_args.engine_root)
is_test_project = False
# Verify the 3rd Party Root Path
@@ -285,26 +285,26 @@ def main(args):
common.ERROR_CODE_INVALID_PARAMETER)
third_party_path = third_party_path.parent
build_dir = verified_dev_root / parsed_args.build_dir
build_dir = parsed_args.build_dir
signing_config = build_optional_signing_profile(store_file=parsed_args.get_argument(SIGNING_PROFILE_STORE_FILE_ARGUMENT_NAME),
store_password=parsed_args.get_argument(SIGNING_PROFILE_STORE_PASSWORD_ARGUMENT_NAME),
key_alias=parsed_args.get_argument(SIGNING_PROFILE_KEY_ALIAS_ARGUMENT_NAME),
key_password=parsed_args.get_argument(SIGNING_PROFILE_KEY_PASSWORD_ARGUMENT_NAME))
logging.debug("Dev Root : %s", str(verified_dev_root.resolve()))
logging.debug("Engine Root : %s", str(verified_engine_root.resolve()))
logging.debug("Build Path : %s", str(build_dir.resolve()))
logging.debug("Android NDK Path : %s", str(verified_android_ndk_path.resolve()))
logging.debug("Android SDK Path : %s", str(verified_android_sdk_path.resolve()))
# Prepare the generator and execute
generator = android_support.AndroidProjectGenerator(dev_root=verified_dev_root,
generator = android_support.AndroidProjectGenerator(engine_root=verified_engine_root,
project_path=verified_project_path,
build_dir=build_dir,
android_sdk_path=verified_android_sdk_path,
android_ndk_path=verified_android_ndk_path,
android_sdk_version=verified_android_sdk_platform,
android_ndk_platform=verified_android_ndk_platform,
game_name=verified_game_name,
third_party_path=third_party_path,
cmake_version=cmake_version,
override_cmake_path=override_cmake_path,
@@ -90,7 +90,6 @@ def test_adb_call(mock_check_output):
patch.object(android_deployment.AndroidDeployment, 'resolve_adb_tool', return_value=pathlib.Path("Foo")), \
patch.object(pathlib.Path, 'glob', return_value=["foo.bar"]):
local_asset_path = pathlib.Path("Foo")
inst = android_deployment.AndroidDeployment(dev_root=TEST_DEV_ROOT,
build_dir=TEST_BUILD_DIR,
configuration='profile',
@@ -115,7 +114,6 @@ def test_adb_shell(mock_adb_call):
patch.object(android_deployment.AndroidDeployment, 'resolve_adb_tool', return_value=pathlib.Path("Foo")), \
patch.object(pathlib.Path, 'glob', return_value=["foo.bar"]):
local_asset_path = pathlib.Path("Foo")
inst = android_deployment.AndroidDeployment(dev_root=TEST_DEV_ROOT,
build_dir=TEST_BUILD_DIR,
configuration='profile',
@@ -141,7 +139,6 @@ def test_adb_ls_success(mock_adb_shell):
patch.object(android_deployment.AndroidDeployment, 'resolve_adb_tool', return_value=pathlib.Path("Foo")), \
patch.object(pathlib.Path, 'glob', return_value=["foo.bar"]):
local_asset_path = pathlib.Path("Foo")
inst = android_deployment.AndroidDeployment(dev_root=TEST_DEV_ROOT,
build_dir=TEST_BUILD_DIR,
configuration='profile',
@@ -168,7 +165,6 @@ def test_adb_ls_error_no_output(mock_adb_shell):
patch.object(android_deployment.AndroidDeployment, 'resolve_adb_tool', return_value=pathlib.Path("Foo")), \
patch.object(pathlib.Path, 'glob', return_value=["foo.bar"]):
local_asset_path = pathlib.Path("Foo")
inst = android_deployment.AndroidDeployment(dev_root=TEST_DEV_ROOT,
build_dir=TEST_BUILD_DIR,
configuration='profile',
@@ -195,7 +191,6 @@ def test_adb_ls_error_no_such_file(mock_adb_shell):
patch.object(android_deployment.AndroidDeployment, 'resolve_adb_tool', return_value=pathlib.Path("Foo")), \
patch.object(pathlib.Path, 'glob', return_value=["foo.bar"]):
local_asset_path = pathlib.Path("Foo")
inst = android_deployment.AndroidDeployment(dev_root=TEST_DEV_ROOT,
build_dir=TEST_BUILD_DIR,
configuration='profile',
@@ -222,7 +217,6 @@ def test_adb_ls_error_permission_denied(mock_adb_shell):
patch.object(android_deployment.AndroidDeployment, 'resolve_adb_tool', return_value=pathlib.Path("Foo")), \
patch.object(pathlib.Path, 'glob', return_value=["foo.bar"]):
local_asset_path = pathlib.Path("Foo")
inst = android_deployment.AndroidDeployment(dev_root=TEST_DEV_ROOT,
build_dir=TEST_BUILD_DIR,
configuration='profile',
@@ -250,7 +244,6 @@ def test_get_target_android_devices(mock_adb_call):
patch.object(android_deployment.AndroidDeployment, 'resolve_adb_tool', return_value=pathlib.Path("Foo")), \
patch.object(pathlib.Path, 'glob', return_value=["foo.bar"]):
local_asset_path = pathlib.Path("Foo")
inst = android_deployment.AndroidDeployment(dev_root=TEST_DEV_ROOT,
build_dir=TEST_BUILD_DIR,
configuration='profile',
@@ -277,7 +270,6 @@ def test_check_known_android_paths_success(mock_adb_ls):
patch.object(android_deployment.AndroidDeployment, 'resolve_adb_tool', return_value=pathlib.Path("Foo")), \
patch.object(pathlib.Path, 'glob', return_value=["foo.bar"]):
local_asset_path = pathlib.Path("Foo")
inst = android_deployment.AndroidDeployment(dev_root=TEST_DEV_ROOT,
build_dir=TEST_BUILD_DIR,
configuration='profile',
@@ -303,7 +295,6 @@ def test_check_known_android_paths_fail(mock_adb_ls):
patch.object(android_deployment.AndroidDeployment, 'resolve_adb_tool', return_value=pathlib.Path("Foo")), \
patch.object(pathlib.Path, 'glob', return_value=["foo.bar"]):
local_asset_path = pathlib.Path("Foo")
inst = android_deployment.AndroidDeployment(dev_root=TEST_DEV_ROOT,
build_dir=TEST_BUILD_DIR,
configuration='profile',
@@ -330,7 +321,6 @@ def test_detect_device_storage_path_no_external_storage_env(mock_check_known_and
patch.object(android_deployment.AndroidDeployment, 'resolve_adb_tool', return_value=pathlib.Path("Foo")),\
patch.object(pathlib.Path, 'glob', return_value=["foo.bar"]):
local_asset_path = pathlib.Path("Foo")
inst = android_deployment.AndroidDeployment(dev_root=TEST_DEV_ROOT,
build_dir=TEST_BUILD_DIR,
configuration='profile',
@@ -357,7 +347,6 @@ def test_detect_device_storage_path_invalid_external_storage_env(mock_check_know
patch.object(android_deployment.AndroidDeployment, 'resolve_adb_tool', return_value=pathlib.Path("Foo")),\
patch.object(pathlib.Path, 'glob', return_value=["foo.bar"]):
local_asset_path = pathlib.Path("Foo")
inst = android_deployment.AndroidDeployment(dev_root=TEST_DEV_ROOT,
build_dir=TEST_BUILD_DIR,
configuration='profile',
@@ -384,7 +373,6 @@ def test_detect_device_storage_path_valid_external_storage_env(mock_adb_ls, mock
patch.object(android_deployment.AndroidDeployment, 'resolve_adb_tool', return_value=pathlib.Path("Foo")),\
patch.object(pathlib.Path, 'glob', return_value=["foo.bar"]):
local_asset_path = pathlib.Path("Foo")
inst = android_deployment.AndroidDeployment(dev_root=TEST_DEV_ROOT,
build_dir=TEST_BUILD_DIR,
configuration='profile',
@@ -427,7 +415,6 @@ def test_detect_device_storage_path_real_path():
patch.object(android_deployment.AndroidDeployment, 'adb_ls', wraps=_mock_adb_ls), \
patch.object(pathlib.Path, 'glob', return_value=["foo.bar"]):
local_asset_path = pathlib.Path("Foo")
inst = android_deployment.AndroidDeployment(dev_root=TEST_DEV_ROOT,
build_dir=TEST_BUILD_DIR,
configuration='profile',
@@ -469,7 +456,6 @@ def test_detect_device_storage_path_real_path_fail(mock_check_known_android_path
patch.object(android_deployment.AndroidDeployment, 'adb_ls', wraps=_mock_adb_ls), \
patch.object(pathlib.Path, 'glob', return_value=["foo.bar"]):
local_asset_path = pathlib.Path("Foo")
inst = android_deployment.AndroidDeployment(dev_root=TEST_DEV_ROOT,
build_dir=TEST_BUILD_DIR,
configuration='profile',
@@ -493,7 +479,6 @@ def test_get_device_file_timestamp_success(mock_adb_shell):
patch.object(android_deployment.AndroidDeployment, 'resolve_adb_tool', return_value=pathlib.Path("Foo")), \
patch.object(pathlib.Path, 'glob', return_value=["foo.bar"]):
local_asset_path = pathlib.Path("Foo")
inst = android_deployment.AndroidDeployment(dev_root=TEST_DEV_ROOT,
build_dir=TEST_BUILD_DIR,
configuration='profile',
@@ -522,7 +507,6 @@ def test_get_device_file_timestamp_no_file(mock_adb_shell):
patch.object(android_deployment.AndroidDeployment, 'resolve_adb_tool', return_value=pathlib.Path("Foo")), \
patch.object(pathlib.Path, 'glob', return_value=["foo.bar"]):
local_asset_path = pathlib.Path("Foo")
inst = android_deployment.AndroidDeployment(dev_root=TEST_DEV_ROOT,
build_dir=TEST_BUILD_DIR,
configuration='profile',
@@ -551,7 +535,6 @@ def test_get_device_file_timestamp_bad_timestamp_file(mock_adb_shell):
patch.object(android_deployment.AndroidDeployment, 'resolve_adb_tool', return_value=pathlib.Path("Foo")), \
patch.object(pathlib.Path, 'glob', return_value=["foo.bar"]):
local_asset_path = pathlib.Path("Foo")
inst = android_deployment.AndroidDeployment(dev_root=TEST_DEV_ROOT,
build_dir=TEST_BUILD_DIR,
configuration='profile',
@@ -576,7 +559,7 @@ def test_get_device_file_timestamp_bad_timestamp_file(mock_adb_shell):
def test_update_device_file_timestamp(tmpdir):
cache_dir = f'{TEST_DEV_ROOT}/Cache/{TEST_GAME_NAME}/{TEST_ASSET_TYPE}'
cache_dir = f'{TEST_DEV_ROOT}/{TEST_GAME_NAME}/Cache/{TEST_ASSET_TYPE}'
tmpdir.ensure(f'{cache_dir}/foo.txt')
mock_dev_root = tmpdir.join(TEST_DEV_ROOT).realpath()
@@ -585,7 +568,6 @@ def test_update_device_file_timestamp(tmpdir):
patch.object(android_deployment.AndroidDeployment, 'resolve_adb_tool', return_value=pathlib.Path("Foo")), \
patch.object(android_deployment.AndroidDeployment, 'adb_call', return_value="") as mock_adb_call:
local_asset_path = pathlib.Path(tmpdir.join(cache_dir).realpath())
inst = android_deployment.AndroidDeployment(dev_root=mock_dev_root,
build_dir=TEST_BUILD_DIR,
configuration='profile',
@@ -624,8 +606,8 @@ def test_execute_success(tmpdir, test_config, test_package_name, test_device_sto
tmpdir.join(f"{TEST_DEV_ROOT}/{TEST_BUILD_DIR}/app/build/outputs/apk/{test_config}/app-{test_config}.apk").ensure()
expected_apk_path = str(tmpdir.join(f"{TEST_DEV_ROOT}/{TEST_BUILD_DIR}/app/build/outputs/apk/{test_config}/app-{test_config}.apk").realpath())
tmpdir.join(f"{TEST_DEV_ROOT}/Cache/{TEST_GAME_NAME}/{TEST_ASSET_TYPE}/dummy.txt").ensure()
expected_asset_path = str(tmpdir.join(f"{TEST_DEV_ROOT}/Cache/{TEST_GAME_NAME}/{TEST_ASSET_TYPE}").realpath())
tmpdir.join(f"{TEST_DEV_ROOT}/{TEST_GAME_NAME}/Cache/{TEST_ASSET_TYPE}/dummy.txt").ensure()
expected_asset_path = str(tmpdir.join(f"{TEST_DEV_ROOT}/{TEST_GAME_NAME}/Cache/{TEST_ASSET_TYPE}").realpath())
tmpdir.join(f"{TEST_DEV_ROOT}/{TEST_BUILD_DIR}/app/src/main/assets/Registry/dummy.txt").ensure()
expected_registry_path = str(tmpdir.join(f"{TEST_DEV_ROOT}/{TEST_BUILD_DIR}/app/src/main/assets/Registry").realpath())
@@ -696,8 +678,8 @@ def test_execute_clean_deploy_success(tmpdir, test_game_name, test_config, test_
tmpdir.join(f"{TEST_DEV_ROOT}/{TEST_BUILD_DIR}/app/build/outputs/apk/{test_config}/app-{test_config}.apk").ensure()
expected_apk_path = str(tmpdir.join(f"{TEST_DEV_ROOT}/{TEST_BUILD_DIR}/app/build/outputs/apk/{test_config}/app-{test_config}.apk").realpath())
tmpdir.join(f"{TEST_DEV_ROOT}/Cache/{test_game_name}/{test_asset_type}/dummy.txt").ensure()
expected_asset_path = str(tmpdir.join(f"{TEST_DEV_ROOT}/Cache/{test_game_name}/{test_asset_type}").realpath())
tmpdir.join(f"{TEST_DEV_ROOT}/{test_game_name}/Cache/{test_asset_type}/dummy.txt").ensure()
expected_asset_path = str(tmpdir.join(f"{TEST_DEV_ROOT}/{test_game_name}/Cache/{test_asset_type}").realpath())
tmpdir.join(f"{TEST_DEV_ROOT}/{TEST_BUILD_DIR}/app/src/main/assets/Registry/dummy.txt").ensure()
expected_registry_path = str(tmpdir.join(f"{TEST_DEV_ROOT}/{TEST_BUILD_DIR}/app/src/main/assets/Registry").realpath())
@@ -792,8 +774,7 @@ def test_execute_incremental_deploy_success(tmpdir, test_config, test_package_na
tmpdir.join(f"{TEST_DEV_ROOT}/{TEST_BUILD_DIR}/app/build/outputs/apk/{test_config}/app-{test_config}.apk").ensure()
expected_apk_path = str(tmpdir.join(f"{TEST_DEV_ROOT}/{TEST_BUILD_DIR}/app/build/outputs/apk/{test_config}/app-{test_config}.apk").realpath())
tmpdir.join(f"{TEST_DEV_ROOT}/Cache/{TEST_GAME_NAME}/{TEST_ASSET_TYPE}/dummy.txt").ensure()
expected_asset_path = str(tmpdir.join(f"{TEST_DEV_ROOT}/Cache/{TEST_GAME_NAME}/{TEST_ASSET_TYPE}").realpath())
tmpdir.join(f"{TEST_DEV_ROOT}/{TEST_GAME_NAME}/Cache/{TEST_ASSET_TYPE}/dummy.txt").ensure()
tmpdir.join(f"{TEST_DEV_ROOT}/{TEST_BUILD_DIR}/app/src/main/assets/Registry/dummy.txt").ensure()
expected_registry_path = str(tmpdir.join(f"{TEST_DEV_ROOT}/{TEST_BUILD_DIR}/app/src/main/assets/Registry").realpath())
@@ -27,7 +27,7 @@ from cmake.Tools.Platform.Android import android_support, generate_android_proje
@pytest.mark.parametrize(
"from_override, version_str, expected_result", [
pytest.param(False, b"Gradle 4.10.1", LooseVersion('4.10.1'), id='equalMinVersion'),
pytest.param(False, b"Gradle 5.6.4", LooseVersion('5.6.4'), id='eualMaxVersion'),
pytest.param(False, b"Gradle 5.6.4", LooseVersion('5.6.4'), id='equalMaxVersion'),
pytest.param(False, b"Gradle 1.0", common.LmbrCmdError('error', common.ERROR_CODE_ENVIRONMENT_ERROR), id='lessThanMinVersion'),
pytest.param(False, b"Gradle 26.3", common.LmbrCmdError('error', common.ERROR_CODE_ENVIRONMENT_ERROR), id='greaterThanMaxVersion'),
pytest.param(True, b"Gradle 4.10.1", LooseVersion('4.10.1')),
+2 -2
View File
@@ -441,7 +441,7 @@ def add_remove_gem(add: bool,
def _run_add_gem(args: argparse) -> int:
return add_remove_gem(True,
common.determine_dev_root(),
common.determine_engine_root(),
args.gem_path,
args.project_path,
args.project_restricted_path,
@@ -451,7 +451,7 @@ def _run_add_gem(args: argparse) -> int:
def _run_remove_gem(args: argparse) -> int:
return add_remove_gem(False,
common.determine_dev_root(),
common.determine_engine_root(),
args.gem_path,
args.project_path,
args.project_restricted_path,
+69 -92
View File
@@ -30,9 +30,7 @@ from cmake.Tools import layout_tool
DEFAULT_TEXT_READ_ENCODING = 'UTF-8' # The default encoding to use when reading from a text file
DEFAULT_TEXT_WRITE_ENCODING = 'ascii' # The encoding to use when writing to a text file
ENCODING_ERROR_HANDLINGS = 'ignore' # What to do if we encounter any encoding errors
DEFAULT_PAK_ROOT = 'Pak' # The default Pak root folder under dev where the game paks are built
ROOT_DEV_PATH = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', '..', '..', '..'))
DEFAULT_PAK_ROOT = 'Pak' # The default Pak root folder under engine root where the game paks are built
if platform.system() == 'Windows':
# Re-use microsoft error codes since this script is meant to only run on windows host platforms
@@ -53,7 +51,7 @@ else:
ERROR_CODE_ENVIRONMENT_ERROR = 1
ERROR_CODE_GENERAL_ERROR = 1
DEV_ROOT_CHECK_FILE = 'engine.json'
ENGINE_ROOT_CHECK_FILE = 'engine.json'
HASH_CHUNK_SIZE = 200000
@@ -75,9 +73,25 @@ class LmbrCmdError(Exception):
return str(self.msg)
def determine_dev_root(starting_path=None):
def read_project_name_from_project_json(project_path):
project_name = None
try:
with (pathlib.Path(project_path) / 'project.json').open('r') as project_file:
project_json = json.load(project_file)
project_name = project_json['project_name']
except OSError as os_error:
logging.warning(f'Unable to open "project.json" file: {os_error}')
except json.JSONDecodeError as json_error:
logging.warning(f'Unable to decode json in {project_file}: {json_error}')
except KeyError as key_error:
logging.warning(f'{project_file} is missing project_name key: {key_error}')
return project_name
def determine_engine_root(starting_path=None):
"""
Determine the dev root of the engine. By default, the dev root is the engine path, which is determined by walking
Determine the engine root of the engine. By default, the engine root is the engine path, which is determined by walking
up the current working directory until we find the engine.json marker
:param starting_path: Optional starting path to look for the engine.json marker file, otherwise use the current working path
@@ -86,14 +100,14 @@ def determine_dev_root(starting_path=None):
current_path = os.path.normpath(starting_path or os.getcwd())
check_file = os.path.join(current_path, DEV_ROOT_CHECK_FILE)
check_file = os.path.join(current_path, ENGINE_ROOT_CHECK_FILE)
while not os.path.isfile(check_file):
next_path = os.path.dirname(current_path)
if next_path == current_path:
# If going up one level results in the same path, we've hit the root
break
check_file = os.path.join(next_path, DEV_ROOT_CHECK_FILE)
check_file = os.path.join(next_path, ENGINE_ROOT_CHECK_FILE)
current_path = next_path
if not os.path.isfile(check_file):
@@ -123,34 +137,34 @@ def get_config_file_values(config_file_path, keys_to_extract):
return result_map
def get_bootstrap_values(dev_root, keys_to_extract):
def get_bootstrap_values(engine_root, keys_to_extract):
"""
Extract requested values from the bootstrap.cfg file in the def root folder
:param dev_root: The dev root folder where bootstrap.cfg exists
:param engine_root: The engine root folder where bootstrap.cfg exists
:param keys_to_extract: The keys to extract into a dictionary
:return: Dictionary of keys and its values (for matched keys)
"""
bootstrap_file = os.path.join(dev_root, 'bootstrap.cfg')
bootstrap_file = os.path.join(engine_root, 'bootstrap.cfg')
if not os.path.isfile(bootstrap_file):
raise LmbrCmdError("Missing 'bootstrap.cfg' file from dev root ('{}')".format(dev_root),
raise LmbrCmdError("Missing 'bootstrap.cfg' file from engine root ('{}')".format(engine_root),
ERROR_CODE_FILE_NOT_FOUND)
result_map = get_config_file_values(bootstrap_file, keys_to_extract)
return result_map
def validate_ap_config_asset_type_enabled(dev_root, bootstrap_asset_type):
def validate_ap_config_asset_type_enabled(engine_root, bootstrap_asset_type):
"""
Validate that the requested bootstrap asset type was enabled in the asset processor configuration file
:param dev_root: The dev root to lookup the AP config file
:param engine_root: The engine root to lookup the AP config file
:param bootstrap_asset_type: The asset type to validate
:return: True if the asset type was enabled, false if not
"""
ap_config_file = os.path.join(dev_root, 'AssetProcessorPlatformConfig.ini')
ap_config_file = os.path.join(engine_root, 'AssetProcessorPlatformConfig.setreg')
if not os.path.isfile(ap_config_file):
raise LmbrCmdError("Missing required asset processor configuration file at '{}'".format(dev_root),
raise LmbrCmdError("Missing required asset processor configuration file at '{}'".format(engine_root),
ERROR_CODE_FILE_NOT_FOUND)
parser = configparser.ConfigParser()
@@ -285,48 +299,44 @@ def verify_tool(override_tool_path, tool_name, tool_filename, argument_name, too
ERROR_CODE_ERROR_NOT_SUPPORTED)
def verify_game_project_and_dev_root(game_name, dev_root):
def verify_project_and_engine_root(project_root, engine_root):
"""
Verify the dev root folder and the game name against that dev root. This will perform basic minimal checks
Verify the engine root folder and the project root folder. This will perform basic minimal checks
for validation:
1. Make sure bootstrap.cfg exists
2. Make sure ${dev_root}/${game_name}/project.json exists
1. Make sure ${engine_root}/engine.json
2. Make sure ${project_root}/project.json exists
3. Make sure that the project.json minimally has a json structure with a 'project_name' attribute
The game name will be verified by returning the value of 'project_name' from the json file to minimize issues
on case-insensitive file systems because we rely on the fact that the game name matches the folder in which it resides
The project name will be verified by returning the value of 'project_name' from the json file to minimize issues
on case-insensitive file systems because we rely on the fact that the project name matches the folder in which it resides
:param game_name: The game name to verify. If None, skip the game name verification
:param dev_root: The dev root directory to verify
:return: A tuple of the actual 'project_name' from the game's project.json and the pathlib.Path of the dev root if verified
:param project_root: The project root to verify. If None, skip the project name verification
:param engine_root: The engine root directory to verify
:return: A tuple of the actual 'project_name' from the game's project.json and the pathlib.Path of the engine root if verified
"""
dev_root_path = pathlib.Path(dev_root)
if not dev_root_path.exists():
raise LmbrCmdError(f"Invalid dev root path ({dev_root})",
engine_root_path = pathlib.Path(engine_root)
if not engine_root_path.exists():
raise LmbrCmdError(f"Invalid engine root path ({engine_root})",
ERROR_CODE_INVALID_PARAMETER)
# Sanity check: bootstrap
bootstrap_path = dev_root_path / 'bootstrap.cfg'
if not bootstrap_path.exists():
raise LmbrCmdError(f"Invalid dev root path ({dev_root}). Missing bootstrap.cfg",
# Sanity check: engine.json
engine_json_path = engine_root_path / ENGINE_ROOT_CHECK_FILE
if not engine_json_path.exists():
raise LmbrCmdError(f"Invalid engine root path ({engine_root}). Missing {ENGINE_ROOT_CHECK_FILE}",
ERROR_CODE_INVALID_PARAMETER)
if game_name is None:
return None, dev_root_path
if project_root is None:
return None, engine_root_path
else:
game_folder = dev_root_path / game_name
game_folder_project_properties = game_folder / 'project.json'
if not game_folder_project_properties.is_file():
raise LmbrCmdError(f"Invalid game '{game_name}'. Make sure it exists under {dev_root}",
project_path = engine_root_path / project_root
project_path_project_properties = project_path / 'project.json'
if not project_path_project_properties.is_file():
raise LmbrCmdError(f'Invalid project at path "{project_path}". It is missing the project.json file',
ERROR_CODE_INVALID_PARAMETER)
try:
with open(game_folder_project_properties) as project_json_file:
project_json = json.load(project_json_file)
return project_json['project_name'], dev_root_path
except (json.JSONDecodeError, KeyError) as e:
raise LmbrCmdError(f"Invalid game '{game_name}'. Its project.json is corrupt or invalid: {str(e)}",
ERROR_CODE_INVALID_PARAMETER)
project_name = read_project_name_from_project_json(project_path)
if not project_name:
raise LmbrCmdError(f'Invalid project at path "{project_path}". Its project.json does not contains a "project_name" key',
ERROR_CODE_INVALID_PARAMETER)
return project_path, engine_root_path
def remove_dir_path(path):
"""
@@ -444,13 +454,13 @@ def validate_build_dir_and_config(build_dir_name, configuration):
return build_dir, build_config_dir
def validate_deployment_arguments(build_dir_name, configuration, game_name):
def validate_deployment_arguments(build_dir_name, configuration, project_path):
"""
Validate the minimal platform deployment arguments
@param build_dir_name: The name of the build directory relative to the current working directory
@param configuration: The configuration the deployment is based on
@param game_name: The name of the game project to deploy
@param project_path: The path the project to deploy
@return: Tuple of (resolved build_dir, game name, asset mode, asset_type, and Pak root folder)
"""
@@ -458,16 +468,16 @@ def validate_deployment_arguments(build_dir_name, configuration, game_name):
platform_settings = PlatformSettings(build_dir)
if not game_name:
if not project_path:
if not platform_settings.projects:
raise LmbrCmdError("Missing required game project argument. Unable to determine a default one.")
game_name = platform_settings.projects[0]
logging.info(f"Using default game project '{game_name}' as the game project")
internal_project_path = pathlib.PurePath(platform_settings.projects[0]).resolve()
logging.info(f"Using project_path '{internal_project_path}' as the game project")
else:
if game_name not in platform_settings.projects:
raise LmbrCmdError(f"Game project {game_name} not valid. Was not configured for build directory {build_dir_name}.")
if project_path not in platform_settings.projects:
raise LmbrCmdError(f"Game project {project_path} not valid. Was not configured for build directory {build_dir_name}.")
return build_dir, game_name, platform_settings.asset_deploy_mode, platform_settings.asset_deploy_type, platform_settings.override_pak_root or DEFAULT_PAK_ROOT
return build_dir, project_path, platform_settings.asset_deploy_mode, platform_settings.asset_deploy_type, platform_settings.override_pak_root or DEFAULT_PAK_ROOT
class CommandLineExec(object):
@@ -547,20 +557,18 @@ class CommandLineExec(object):
raise LmbrCmdError(f"Error trying to call '{self.executable_path}': {str(err)}")
def sync_platform_layout(platform_name, game_project, asset_mode, asset_type, layout_root):
def sync_platform_layout(platform_name, project_path, asset_mode, asset_type, layout_root):
"""
Perform a layout sync directly on the game project for a platform, game project, asset mode, asset type
@param platform_name: The platform (lower) name to sync from
@param game_project: The game project to sync to
@param project_path: The path to project to sync to
@param asset_mode: The asset mode to base the sync on
@param asset_type: The asset type to base the sync on
@param layout_root: The root of the layout to sync to
@param record_elapsed: Option to output the elapsed time
"""
layout_tool.ASSET_SYNC_MODE_FUNCTION[asset_mode](dev_root=ROOT_DEV_PATH,
target_platform=platform_name,
game=game_project,
layout_tool.ASSET_SYNC_MODE_FUNCTION[asset_mode](target_platform=platform_name,
project_path=project_path,
asset_type=asset_type,
warning_on_missing_assets=True,
layout_target=layout_root,
@@ -603,37 +611,6 @@ def get_cmake_dependency_modules(build_dir_path, target, module_type):
return dep_modules
GAME_FOLDER_REGEX = re.compile(r"sys_game_folder\s*=\s*(.*)")
GAME_NAME_REGEX = re.compile(r"sys_game_name\s*=\s*(.*)")
def transform_bootstrap_for_game(game_name, src_bootstrap, dst_bootstrap):
"""
Given a source bootstrap.cfg and game, write a copy of one to a different destination and transform it to
override its 'sys_game_folder' or 'sys_game_name' to match the input game_name
:param game_name: The name of the game to set in the destination bootstrap
:param src_bootstrap: The absolute path of the source bootstrap
:param dst_bootstrap: The absolute path of the destination bootstrap file to write to (or overwrite)
"""
with open(src_bootstrap, "r") as src_bootstrap_file:
bootstrap_lines = src_bootstrap_file.readlines()
with open(dst_bootstrap, "w") as dst_bootstrap_file:
sys_game_detected = False
for bootstrap_line in bootstrap_lines:
if GAME_FOLDER_REGEX.match(bootstrap_line):
dst_bootstrap_file.write(f'sys_game_folder={game_name}\n')
sys_game_detected = True
elif GAME_NAME_REGEX.match(bootstrap_line):
dst_bootstrap_file.write(f'sys_game_name={game_name}\n')
sys_game_detected = True
else:
dst_bootstrap_file.write(bootstrap_line)
if not sys_game_detected:
# If no sys_game* is detected, inject one to prevent an error at least in the target
dst_bootstrap_file.write(f'sys_game_folder={game_name}\n')
def get_test_module_registry(build_dir_path):
"""
Read a test module registry file for for all test modules that are enabled for a target build directory
+9 -9
View File
@@ -36,7 +36,7 @@ def set_current_project(dev_root: str,
try:
with open(os.path.join(dev_root, 'bootstrap.cfg'), 'r') as s:
data = s.read()
data = re.sub(r'(.*sys_game_folder\s*?[=:]\s*?)([^\n]+)\n', r'\1 {}\n'.format(project_path),
data = re.sub(r'(.*project_path\s*?[=:]\s*?)([^\n]+)\n', r'\1 {}\n'.format(project_path),
data, flags=re.IGNORECASE)
if os.path.isfile(os.path.join(dev_root, 'bootstrap.cfg')):
os.unlink(os.path.join(dev_root, 'bootstrap.cfg'))
@@ -52,29 +52,29 @@ def get_current_project(dev_root: str) -> str:
"""
get what the current project set is
:param dev_root: the dev root of the engine
:return: sys_game_folder or None on failure
:return: project_path or None on failure
"""
try:
with open(os.path.join(dev_root, 'bootstrap.cfg'), 'r') as s:
data = s.read()
sys_game_folder = re.search(r'(.*sys_game_folder\s*?[=:]\s*?)(?P<sys_game_folder>[^\n]+)\n',
data, flags=re.IGNORECASE).group('sys_game_folder').strip()
project_path = re.search(r'(.*project_path\s*?[=:]\s*?)(?P<project_path>[^\n]+)\n',
data, flags=re.IGNORECASE).group('project_path').strip()
except Exception as e:
logger.error('Failed to get current project. Exception: ' + str(e))
return ''
return sys_game_folder
return project_path
def _run_get_current_project(args: argparse) -> int:
sys_game_folder = get_current_project(common.determine_dev_root())
if sys_game_folder:
print(sys_game_folder)
project_path = get_current_project(common.determine_engine_root())
if project_path:
print(project_path)
return 0
return 1
def _run_set_current_project(args: argparse) -> int:
return set_current_project(common.determine_dev_root(), args.project_path)
return set_current_project(common.determine_engine_root(), args.project_path)
def add_args(parser, subparsers) -> None:
+4 -28
View File
@@ -416,17 +416,11 @@ def create_template(dev_root: str,
source_restricted_path = source_restricted_path.replace('\\', '/')
if not os.path.isabs(source_restricted_path):
source_restricted_path = f'{dev_root}/{source_restricted_path}'
if not os.path.isdir(source_restricted_path):
logger.error(f'Src restricted path {source_restricted_path} is not a folder.')
return 1
# template_restricted_path
template_restricted_path = template_restricted_path.replace('\\', '/')
if not os.path.isabs(template_restricted_path):
template_restricted_path = f'{dev_root}/{template_restricted_path}'
if not os.path.isdir(template_restricted_path):
logger.error(f'Template restricted path {template_restricted_path} is not a folder.')
return 1
# source restricted relative
source_restricted_platform_relative_path = source_restricted_platform_relative_path.replace('\\', '/')
@@ -1015,17 +1009,11 @@ def create_from_template(dev_root: str,
destination_restricted_path = destination_restricted_path.replace('\\', '/')
if not os.path.isabs(destination_restricted_path):
destination_restricted_path = f'{dev_root}/{destination_restricted_path}'
if not os.path.isdir(destination_restricted_path):
logger.error(f'Dst restricted path {destination_restricted_path} is not a folder.')
return 1
# template_restricted_path
template_restricted_path = template_restricted_path.replace('\\', '/')
if not os.path.isabs(template_restricted_path):
template_restricted_path = f'{dev_root}/{template_restricted_path}'
if not os.path.isdir(template_restricted_path):
logger.error(f'Template restricted path {template_restricted_path} is not a folder.')
return 1
# destination restricted relative
destination_restricted_platform_relative_path = destination_restricted_platform_relative_path.replace('\\', '/')
@@ -1137,17 +1125,11 @@ def create_project(dev_root: str,
project_restricted_path = project_restricted_path.replace('\\', '/')
if not os.path.isabs(project_restricted_path):
project_restricted_path = f'{dev_root}/{project_restricted_path}'
if not os.path.isdir(project_restricted_path):
logger.error(f'Project restricted path {project_restricted_path} is not a folder.')
return 1
# template_restricted_path
template_restricted_path = template_restricted_path.replace('\\', '/')
if not os.path.isabs(template_restricted_path):
template_restricted_path = f'{dev_root}/{template_restricted_path}'
if not os.path.isdir(template_restricted_path):
logger.error(f'Template restricted path {template_restricted_path} is not a folder.')
return 1
# project restricted relative
project_restricted_platform_relative_path = project_restricted_platform_relative_path.replace('\\', '/')
@@ -1313,17 +1295,11 @@ def create_gem(dev_root: str,
gem_restricted_path = gem_restricted_path.replace('\\', '/')
if not os.path.isabs(gem_restricted_path):
gem_restricted_path = f'{dev_root}/{gem_restricted_path}'
if not os.path.isdir(gem_restricted_path):
logger.error(f'Gem restricted path {gem_restricted_path} is not a folder.')
return 1
# template_restricted_path
template_restricted_path = template_restricted_path.replace('\\', '/')
if not os.path.isabs(template_restricted_path):
template_restricted_path = f'{dev_root}/{template_restricted_path}'
if not os.path.isdir(template_restricted_path):
logger.error(f'Template restricted path {template_restricted_path} is not a folder.')
return 1
# gem restricted relative
gem_restricted_platform_relative_path = gem_restricted_platform_relative_path.replace('\\', '/')
@@ -1385,7 +1361,7 @@ def create_gem(dev_root: str,
def _run_create_template(args: argparse) -> int:
return create_template(common.determine_dev_root(),
return create_template(common.determine_engine_root(),
args.source_path,
args.template_path,
args.source_restricted_path,
@@ -1398,7 +1374,7 @@ def _run_create_template(args: argparse) -> int:
def _run_create_from_template(args: argparse) -> int:
return create_from_template(common.determine_dev_root(),
return create_from_template(common.determine_engine_root(),
args.destination_path,
args.template_path,
args.destination_restricted_path,
@@ -1411,7 +1387,7 @@ def _run_create_from_template(args: argparse) -> int:
def _run_create_project(args: argparse) -> int:
return create_project(common.determine_dev_root(),
return create_project(common.determine_engine_root(),
args.project_path,
args.template_path,
args.project_restricted_path,
@@ -1427,7 +1403,7 @@ def _run_create_project(args: argparse) -> int:
def _run_create_gem(args: argparse) -> int:
return create_gem(common.determine_dev_root(),
return create_gem(common.determine_engine_root(),
args.gem_path,
args.template_path,
args.gem_restricted_path,
+2 -2
View File
@@ -176,10 +176,10 @@ def main(args):
parser.add_argument('-b', '--binfolder',
help='The relative location of the binary folder that contains the resource compiler and asset processor')
bootstrap = common.get_bootstrap_values(DEV_ROOT, ['sys_game_folder'])
bootstrap = common.get_bootstrap_values(DEV_ROOT, ['project_path'])
parser.add_argument('-g', '--game-name',
help='The name of the Game whose asset pak will be generated for',
default=bootstrap.get('sys_game_folder'))
default=bootstrap.get('project_path'))
parser.add_argument('-p', '--asset-platform',
help='The asset platform type to process')
+185 -201
View File
@@ -11,14 +11,13 @@
import argparse
import datetime
import hashlib
import logging
import os
import pathlib
import platform
import re
import shutil
import stat
import subprocess
import sys
import tempfile
@@ -26,9 +25,9 @@ import timeit
# Resolve the common python module
ROOT_DEV_PATH = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', '..'))
if ROOT_DEV_PATH not in sys.path:
sys.path.append(ROOT_DEV_PATH)
ROOT_ENGINE_PATH = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', '..'))
if ROOT_ENGINE_PATH not in sys.path:
sys.path.append(ROOT_ENGINE_PATH)
from cmake.Tools import common
@@ -51,21 +50,20 @@ PAK_ONLY_BUILD_CONFIGS = ['RELEASE']
PLATFORM_NAME = platform.system()
# List of files to blacklist from copying to the layout folder
COPY_ASSET_FILE_GENERAL_BLACKLIST_FILES = [
# List of files to deny from copying to the layout folder
COPY_ASSET_FILE_GENERAL_DENYLIST_FILES = [
'aztest_bootstrap.json',
'editor.cfg',
'assetprocessorplatformconfig.ini',
'assetprocessorplatformconfig.setreg',
]
def verify_layout(layout_dir, platform_name, game_name, asset_mode, asset_type):
def verify_layout(layout_dir, platform_name, project_path, asset_mode, asset_type):
"""
Verify a layout folder (WRT to assets and configs) against the bootstrap and system config files
@param layout_dir: The layout path to validate the asset mode against the bootstrap and system configs
@param platform_name: The name of the platform the deployment is for
@param game_name: The game (project) name being deployed
@param project_path: The path to the project being deployed
@param asset_mode: The desired asset mode (PAK, LOOSE, VFS)
@param asset_type: The asset type
@return: The number of possible errors in the configuration files based on the asset mode and type
@@ -98,8 +96,15 @@ def verify_layout(layout_dir, platform_name, game_name, asset_mode, asset_type):
warning_count = 0
# Look up the project_path from the project.json file
project_name = common.read_project_name_from_project_json(project_path)
# If the project-name could not be read from the project.json, then the supplied project path does not
# point to a valid project
if not project_name:
return 1
platform_name_lower = platform_name.lower()
game_name_lower = game_name.lower()
project_name_lower = project_path.lower()
layout_path = pathlib.Path(layout_dir)
# Validate bootstrap.cfg exists
@@ -108,9 +113,7 @@ def verify_layout(layout_dir, platform_name, game_name, asset_mode, asset_type):
warning_count += _warn(f"'bootstrap.cfg' is missing from {str(layout_path)}")
bootstrap_values = None
else:
bootstrap_values = common.get_config_file_values(str(bootstrap_file), ['sys_game_folder',
'sys_game_name',
f'{platform_name_lower}_remote_filesystem',
bootstrap_values = common.get_config_file_values(str(bootstrap_file), [f'{platform_name_lower}_remote_filesystem',
f'{platform_name_lower}_connect_to_remote',
f'{platform_name_lower}_wait_for_connect',
f'{platform_name_lower}_assets',
@@ -120,9 +123,9 @@ def verify_layout(layout_dir, platform_name, game_name, asset_mode, asset_type):
])
# Validate the system_{platform}_{asset type}.cfg exists
platform_system_cfg_file = layout_path / f'system_{platform_name}_{asset_type}.cfg'
platform_system_cfg_file = layout_path / f'system_{platform_name_lower}_{asset_type}.cfg'
if not platform_system_cfg_file.is_file():
warning_count += _warn(f"'system_{platform_name}_{asset_type}.cfg' is missing from {str(layout_path)}")
warning_count += _warn(f"'system_{platform_name_lower}_{asset_type}.cfg' is missing from {str(layout_path)}")
system_config_values = None
else:
system_config_values = common.get_config_file_values(str(platform_system_cfg_file), ['r_ShadersRemoteCompiler',
@@ -133,14 +136,7 @@ def verify_layout(layout_dir, platform_name, game_name, asset_mode, asset_type):
if bootstrap_values:
remote_ip = bootstrap_values.get(f'{platform_name_lower}_remote_ip') or bootstrap_values.get('remote_ip') or LOCAL_HOST
remote_connect = bootstrap_values.get(f'{platform_name}_connect_to_remote') or '0'
# Validate that the game name matches in bootstrap.cfg
bootstrap_game = bootstrap_values.get('sys_game_folder') or bootstrap_values.get('sys_game_name')
if not bootstrap_game:
warning_count += _warn("'bootstrap.cfg' is missing the game name set in 'sys_game_folder")
elif bootstrap_game != game_name:
warning_count += _warn(f"The game specified in bootstrap.cfg ({bootstrap_game}) does not match the game name specified for this deployment ({game_name})")
remote_connect = bootstrap_values.get(f'{platform_name_lower}_connect_to_remote') or '0'
# Validate that the asset type for the platform matches the one set for the build
bootstrap_asset_type = bootstrap_values.get(f'{platform_name_lower}_assets') or bootstrap_values.get('assets')
@@ -152,9 +148,9 @@ def verify_layout(layout_dir, platform_name, game_name, asset_mode, asset_type):
# Validate that if '<platform>_connect_to_remote is enabled, that the 'remote_ip' is not set to local host
warning_count += _validate_remote_ap(remote_ip, remote_connect, None)
game_asset_path = layout_path / game_name_lower
if not game_asset_path.is_dir():
warning_count += _warn(f"Asset folder for game {game_name} is missing from the deployment layout.")
project_asset_path = layout_path / project_name_lower
if not project_asset_path.is_dir():
warning_count += _warn(f"Asset folder for project {project_name} is missing from the deployment layout.")
elif system_config_values is not None:
@@ -183,9 +179,9 @@ def verify_layout(layout_dir, platform_name, game_name, asset_mode, asset_type):
# Validate that we have pak files
pak_count = 0
has_shader_pak = False
game_paks = game_asset_path.glob("*.pak")
for game_pak in game_paks:
if game_pak.name == 'shadercachestartup.pak':
project_paks = project_asset_path.glob("*.pak")
for project_pak in project_paks:
if project_pak.name == 'shadercachestartup.pak':
has_shader_pak = True
pak_count += 1
@@ -196,7 +192,7 @@ def verify_layout(layout_dir, platform_name, game_name, asset_mode, asset_type):
# If the shader paks are set, make sure that the remote shader compiler connection settings are set
# or that it is going through AP
if shaders_remote_compiler == '1':
warning_count += _warn(f"Shader paks are set for game {game_name} but remote shader compiling "
warning_count += _warn(f"Shader paks are set for project {project_name} but remote shader compiling "
f"(r_ShadersRemoteCompiler) is still enabled "
f"for it in system_{platform_name_lower}_{asset_type}.cfg.")
else:
@@ -205,7 +201,7 @@ def verify_layout(layout_dir, platform_name, game_name, asset_mode, asset_type):
warning_count += _validate_remote_ap(remote_ip, remote_connect, False)
if shaders_allow_compilation is not None and shaders_allow_compilation == '1':
warning_count += _warn(f"Shader paks are set for game {game_name} but shader compiling "
warning_count += _warn(f"Shader paks are set for project {project_name} but shader compiling "
f"(r_ShadersAllowCompilation) is still enabled "
f"for it in system_{platform_name_lower}_{asset_type}.cfg.")
@@ -226,26 +222,23 @@ def verify_layout(layout_dir, platform_name, game_name, asset_mode, asset_type):
return warning_count
def copy_asset_files_to_layout(game_name, game_asset_folder, target_platform, layout_target):
def copy_asset_files_to_layout(project_asset_folder, target_platform, layout_target):
"""
Perform the specific rules for copying files to the root level of the layout.
:param game_name: The name of the game
:param game_asset_folder: The source game asset folder to copy the files. (Will not traverse deeper than this folder)
:param target_platform: The target platform of the layout
:param layout_target: The target path of the target layout folder.
:param project_asset_folder: The source project asset folder to copy the files. (Will not traverse deeper than this folder)
:param target_platform: The target platform of the layout
:param layout_target: The target path of the target layout folder.
"""
src_asset_contents = os.listdir(game_asset_folder)
src_asset_contents = os.listdir(project_asset_folder)
allowed_system_config_prefix = 'system_{}'.format(target_platform.lower())
for src_file in src_asset_contents:
# For each source file found in the root of the source game asset folder, apply various rules to determine
# For each source file found in the root of the source project asset folder, apply various rules to determine
# if we will copy the file to the layout destination or not
if src_file in COPY_ASSET_FILE_GENERAL_BLACKLIST_FILES:
# The source file is black-listed from being copied
if src_file in COPY_ASSET_FILE_GENERAL_DENYLIST_FILES:
# The source file is denied from being copied
continue
if src_file.startswith('system_'):
@@ -255,7 +248,7 @@ def copy_asset_files_to_layout(game_name, game_asset_folder, target_platform, la
continue
# Resolve the absolute paths for source and destination to perform more specific checks
abs_src = os.path.join(game_asset_folder, src_file)
abs_src = os.path.join(project_asset_folder, src_file)
abs_dst = os.path.join(layout_target, src_file)
if os.path.isdir(abs_src):
@@ -280,35 +273,33 @@ def copy_asset_files_to_layout(game_name, game_asset_folder, target_platform, la
src_file,
src_hash)
continue
if os.path.basename(abs_src) == 'bootstrap.cfg':
logging.debug("Copying (%s) %s -> %s", game_name, abs_src, abs_dst)
common.transform_bootstrap_for_game(game_name, abs_src, abs_dst)
else:
logging.debug("Copying %s -> %s", abs_src, abs_dst)
shutil.copy2(abs_src, abs_dst)
def remove_link(link):
def remove_link(link:pathlib.PurePath):
"""
Helper function to either remove a symlink, or remove a folder
"""
link = pathlib.PurePath(link)
if os.path.isdir(link):
try:
os.unlink(link)
except:
if PLATFORM_NAME == 'Windows':
rmdir_cmd = ['cmd', '/c', 'rmdir', '/J', '/S', link]
else:
rmdir_cmd = ['rm', '-rf', link]
except OSError:
# If unlink fails use shutil.rmtree
def remove_readonly(func, path, _):
"Clear the readonly bit and reattempt the removal"
os.chmod(path, stat.S_IWRITE)
func(path)
try:
logging.debug('Executing call %s', ' '.join(rmdir_cmd))
subprocess.check_call(rmdir_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except subprocess.CalledProcessError as e:
raise common.LmbrCmdError("Error trying remove directory {}: {}".format(link, e),
e.returncode)
shutil.rmtree(link, onerror=remove_readonly)
except shutil.Error as shutil_error:
raise common.LmbrCmdError(f'Error trying remove directory {link}: {shutil_error}', shutil_error.errno)
def create_link(src, tgt, copy):
def create_link(src:pathlib.Path, tgt:pathlib.Path, copy):
"""
Helper function to create a directory link or copy a directory. On windows, this will be a directory junction, and on mac/linux
this will be a soft link
@@ -317,68 +308,74 @@ def create_link(src, tgt, copy):
:param tgt: The target of the new link
:param copy: Perform a directory copy instead of a link
"""
src = pathlib.Path(src)
tgt = pathlib.Path(tgt)
if copy:
if os.path.isdir(tgt):
os.rmdir(tgt)
logging.debug("Copying from %s to %s", src, tgt)
shutil.copytree(src, tgt, symlinks=False)
else:
logging.debug('Creating internal junction %s => %s in %s', src, tgt)
if PLATFORM_NAME == 'Windows':
link_type = 'junction'
junction_cmd = ['cmd', '/c', 'mklink', '/J', tgt, src]
# Remove the exist target
if tgt.is_symlink():
tgt.unlink()
else:
link_type = 'soft link'
junction_cmd = ['ln', '-s', src, tgt]
def remove_readonly(func, path, _):
"Clear the readonly bit and reattempt the removal"
os.chmod(path, stat.S_IWRITE)
func(path)
shutil.rmtree(tgt, onerror=remove_readonly)
logging.debug(f'Copying from {src} to {tgt}')
shutil.copytree(str(src), str(tgt), symlinks=False)
else:
link_type = "symlink"
logging.debug(f'Creating symlink {src} =>{tgt}')
try:
logging.debug('Executing call %s', ' '.join(junction_cmd))
subprocess.check_call(junction_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except subprocess.CalledProcessError as e:
raise common.LmbrCmdError(f"Error trying to create {link_type} {src} => {tgt} : {str(e)}", e.returncode)
if PLATFORM_NAME == "Windows":
link_type = "junction"
import _winapi
_winapi.CreateJunction(str(src), str(tgt))
else:
src.symlink_to(tgt, target_is_directory=True)
except OSError as e:
raise common.LmbrCmdError(f"Error trying to create {link_type} {src} => {tgt} : {e}", e.errno)
def construct_and_validate_cache_game_asset_folder(dev_root, game_name, asset_type, warn_on_missing_game_cache):
def construct_and_validate_cache_project_asset_folder(project_path, asset_type, warn_on_missing_project_cache):
"""
Given the parameters for a game (name, dev root, asset type), construct and validate the absolute path
Given the parameters for a project (project_path, asset type), construct and validate the absolute path
of where the built assets are (for LOOSE and VFS modes)
:param dev_root: The dev root to base the Cache folder search
:param game_name: The name of the game
:param asset_type: The type of asset
:param warn_on_missing_game_cache: Option to warn if the path is missing vs raising an exception
:return: The validated constructed cache game asset folder if it exists, None if not
:param project_path: The path to the project
:param asset_type: The type of asset
:param warn_on_missing_project_cache: Option to warn if the path is missing vs raising an exception
:return: The validated constructed cache project asset folder if it exists, None if not
"""
# Locate the Cache root folder
cache_game_folder_root = os.path.join(dev_root, CACHE_FOLDER_NAME, game_name)
if not os.path.isdir(cache_game_folder_root) and not warn_on_missing_game_cache:
cache_project_folder_root = os.path.join(project_path, CACHE_FOLDER_NAME)
if not os.path.isdir(cache_project_folder_root) and not warn_on_missing_project_cache:
raise common.LmbrCmdError(
"Missing Cache folder for the game in the current dev root. Make sure that assets have been built "
"for the game '{}'".format(game_name),
f"Missing Cache folder for the project at path {project_path}. Make sure that assets have been built ",
common.ERROR_CODE_ERROR_DIRECTORY)
# Locate based on the game's built asset type
cache_game_asset_folder = os.path.join(cache_game_folder_root, asset_type)
if os.path.isdir(cache_game_asset_folder):
# Locate based on the project's built asset type
cache_project_asset_folder = os.path.join(cache_project_folder_root, asset_type)
if os.path.isdir(cache_project_asset_folder):
# TODO: Note, this is only checking the existence of the folder, not for any content validation
return cache_game_asset_folder
return cache_project_asset_folder
# Expected source of the game assets was not found
if not warn_on_missing_game_cache:
# Expected source of the project assets was not found
if not warn_on_missing_project_cache:
raise common.LmbrCmdError(
"Missing compiled assets folder for the game. Make sure that assets for '{}' have been built "
"for the game '{}'".format(asset_type, game_name),
f'Missing compiled assets folder for the project at path {project_path}."'
f' Make sure that assets for "{asset_type}" have been built',
common.ERROR_CODE_ERROR_DIRECTORY)
return None
def sync_layout_vfs(dev_root, target_platform, game, asset_type, warning_on_missing_assets, layout_target, override_pak_folder, copy):
def sync_layout_vfs(target_platform, project_path, asset_type, warning_on_missing_assets, layout_target, override_pak_folder, copy):
"""
Perform the logic to sync the layout folder with assets in VFS mode
:param dev_root: The configured dev root
:param target_platform: The target platform the layout is based on
:param game: The name of the game being synced
:param project_path: The path to the project being synced
:param asset_type: The asset type being synced
:param warning_on_missing_assets: If the built assets cannot be located (LOOSE or PAKs), then optionally warn vs raising an error
:param layout_target: The target layout folder to perform the sync on
@@ -386,71 +383,68 @@ def sync_layout_vfs(dev_root, target_platform, game, asset_type, warning_on_miss
:param copy: Option to copy instead of attempting to symlink/junction
"""
logging.debug("Syncing VFS layout for game '%s' to layout path '%s'", game, layout_target)
logging.debug(f'Syncing VFS layout for project at path "{project_path}" to layout path "{layout_target}"')
game_asset_folder = construct_and_validate_cache_game_asset_folder(dev_root=dev_root,
game_name=game,
project_asset_folder = construct_and_validate_cache_project_asset_folder(project_path=project_path,
asset_type=asset_type,
warn_on_missing_game_cache=warning_on_missing_assets)
warn_on_missing_project_cache=warning_on_missing_assets)
game_folder = game.lower()
vfs_asset_source = os.path.join(game_asset_folder, game_folder, 'config')
vfs_asset_source = os.path.join(project_asset_folder, 'config')
if not os.path.isdir(vfs_asset_source):
raise common.LmbrCmdError("Cache folder for the game '{}' missing 'config' folder".format(game),
raise common.LmbrCmdError("Cache folder for the project '{}' missing 'config' folder".format(project_path),
common.ERROR_CODE_ERROR_DIRECTORY)
# create a temporary folder that will serve as a working junction point into the layout
hasher = hashlib.md5()
hasher.update(dev_root.encode('UTF-8'))
hasher.update(game_folder.encode('UTF-8'))
hasher.update(project_path.encode('UTF-8'))
result = hasher.hexdigest()
temp_dir = tempfile.gettempdir()
temp_vfs_layout_path = os.path.join(temp_dir, 'ly-layout-{}'.format(result), 'vfs')
temp_vfs_layout_game_path = os.path.join(temp_vfs_layout_path, game_folder)
temp_vfs_layout_project_path = temp_vfs_layout_path
temp_vfs_layout_game_config_path = os.path.join(temp_vfs_layout_game_path, 'config')
temp_vfs_layout_project_config_path = os.path.join(temp_vfs_layout_project_path, 'config')
# If the temporary folder was created previously, always reset it
if os.path.isdir(temp_vfs_layout_game_path):
if os.path.isdir(temp_vfs_layout_game_config_path):
os.rmdir(temp_vfs_layout_game_config_path)
shutil.rmtree(temp_vfs_layout_game_path)
os.makedirs(temp_vfs_layout_game_path, exist_ok=True)
if os.path.isdir(temp_vfs_layout_project_path):
if os.path.isdir(temp_vfs_layout_project_config_path):
os.rmdir(temp_vfs_layout_project_config_path)
shutil.rmtree(temp_vfs_layout_project_path)
os.makedirs(temp_vfs_layout_project_path, exist_ok=True)
# Create the 'project asset platform cache' junction before copying configuration files at the engine root to it
layout_project_folder_target = layout_target
# Remove previous layout folder if it is a directory
if os.path.isdir(layout_project_folder_target):
remove_link(layout_project_folder_target)
if os.path.isdir(temp_vfs_layout_project_path):
create_link(temp_vfs_layout_project_path, layout_project_folder_target, copy)
# Create the link
create_link(vfs_asset_source, temp_vfs_layout_game_config_path, copy)
create_link(vfs_asset_source, temp_vfs_layout_project_config_path, copy)
# Create the assets to the layout
copy_asset_files_to_layout(game_name=game,
game_asset_folder=game_asset_folder,
copy_asset_files_to_layout(project_asset_folder=project_asset_folder,
target_platform=target_platform,
layout_target=layout_target)
# Reset the 'gems' junction if any in the layout
layout_gems_folder_src = os.path.join(game_asset_folder, 'gems')
layout_gems_folder_src = os.path.join(project_asset_folder, 'gems')
layout_gems_folder_target = os.path.join(layout_target, 'gems')
if os.path.isdir(layout_gems_folder_target):
remove_link(layout_gems_folder_target)
if os.path.isdir(layout_gems_folder_src):
create_link(layout_gems_folder_src, layout_gems_folder_target, copy)
# Reset the <game_folder> junction
layout_game_folder_target = os.path.join(layout_target, game_folder)
if os.path.isdir(layout_game_folder_target):
remove_link(layout_game_folder_target)
if os.path.isdir(temp_vfs_layout_game_path):
create_link(temp_vfs_layout_game_path, layout_game_folder_target, copy)
def sync_layout_non_vfs(mode, target_platform, dev_root, game, asset_type, warning_on_missing_assets, layout_target, override_pak_folder, copy):
def sync_layout_non_vfs(mode, target_platform, project_path, asset_type, warning_on_missing_assets, layout_target, override_pak_folder, copy):
"""
Perform the logic to sync the layout folder with assets in non-VFS mode (LOOSE or PAK)
:param mode: 'LOOSE' or 'PAK' mode
:param target_platform: The target platform the layout is based on
:param dev_root: The configured dev root
:param game: The name of the game being synced
:param project_path: The path to the project being synced
:param asset_type: The asset type being synced
:param warning_on_missing_assets: If the built assets cannot be located (LOOSE or PAKs), then optionally warn vs raising an error
:param layout_target: The target layout folder to perform the sync on
@@ -460,66 +454,67 @@ def sync_layout_non_vfs(mode, target_platform, dev_root, game, asset_type, warni
assert mode in (ASSET_MODE_PAK, ASSET_MODE_LOOSE)
game_folder = game.lower()
project_name = common.read_project_name_from_project_json(project_path)
if not project_name:
raise common.LmbrCmdError(f'Project at path {project_path} does not have a valid project.json')
project_name_lower = project_name.lower()
layout_gems_folder_target = os.path.join(layout_target, 'gems')
if os.path.isdir(layout_gems_folder_target):
remove_link(layout_gems_folder_target)
layout_game_folder_target = os.path.join(layout_target, game_folder)
if os.path.isdir(layout_game_folder_target):
remove_link(layout_game_folder_target)
if mode == ASSET_MODE_PAK:
target_pak_folder_name = '{}_{}_paks'.format(game_folder, asset_type)
game_asset_folder = os.path.join(dev_root, override_pak_folder or PAK_FOLDER_NAME, target_pak_folder_name)
if not os.path.isdir(game_asset_folder):
target_pak_folder_name = '{}_{}_paks'.format(project_name_lower, asset_type)
project_asset_folder = os.path.join(project_path, override_pak_folder or PAK_FOLDER_NAME, target_pak_folder_name)
if not os.path.isdir(project_asset_folder):
if warning_on_missing_assets:
logging.warning("Pak folder for the game '{}' is missing (expected at '{}'). Skipping layout sync".format(game, game_asset_folder))
logging.warning(f'Pak folder for the project at path "{project_path}" is missing'
f' (expected at "{project_asset_folder}"). Skipping layout sync')
return
else:
raise common.LmbrCmdError("Pak folder for the game '{}' is missing (expected at '{}')".format(game, game_asset_folder),
raise common.LmbrCmdError(f'Pak folder for the project at path "{project_path}" is missing (expected at'
f' "{project_asset_folder}")',
common.ERROR_CODE_ERROR_DIRECTORY)
elif mode == ASSET_MODE_LOOSE:
game_asset_folder = construct_and_validate_cache_game_asset_folder(dev_root=dev_root,
game_name=game,
project_asset_folder = construct_and_validate_cache_project_asset_folder(project_path=project_path,
asset_type=asset_type,
warn_on_missing_game_cache=warning_on_missing_assets)
if not game_asset_folder:
warn_on_missing_project_cache=warning_on_missing_assets)
if not project_asset_folder:
logging.warning(
"Cannot locate built assets for game '{}' (expected at '{}'). Skipping layout sync".format(game,
game_asset_folder))
f'Cannot locate built assets for project at path "{project_path}" (expected at "{project_asset_folder}").'
f' Skipping layout sync')
return
else:
assert False, "Invalid Mode {}".format(mode)
# Create the 'project asset platform cache' junction before copying additional files to it
layout_project_folder_src = project_asset_folder
# Remove previous layout folder if it is a directory
if os.path.isdir(layout_target):
remove_link(layout_target)
if os.path.isdir(layout_project_folder_src):
create_link(layout_project_folder_src, layout_target, copy)
# Create the assets to the layout
copy_asset_files_to_layout(game_name=game,
game_asset_folder=game_asset_folder,
copy_asset_files_to_layout(project_asset_folder=project_asset_folder,
target_platform=target_platform,
layout_target=layout_target)
# Reset the 'gems' junction if any in the layout (only in loose mode).
layout_gems_folder_src = os.path.join(game_asset_folder, 'gems')
layout_gems_folder_src = os.path.join(project_asset_folder, 'gems')
# The gems link only is valid in LOOSE mode. If in PAK, then dont re-link
if mode == ASSET_MODE_LOOSE and os.path.isdir(layout_gems_folder_src):
if os.path.isdir(layout_gems_folder_src):
create_link(layout_gems_folder_src, layout_gems_folder_target, copy)
# Reset the <game_folder> junction
layout_game_folder_src = os.path.join(game_asset_folder, game_folder)
if os.path.isdir(layout_game_folder_src):
create_link(layout_game_folder_src, layout_game_folder_target, copy)
def sync_layout_pak(dev_root, target_platform, game, asset_type, warning_on_missing_assets, layout_target,
def sync_layout_pak(target_platform, project_path, asset_type, warning_on_missing_assets, layout_target,
override_pak_folder, copy):
sync_layout_non_vfs(mode=ASSET_MODE_PAK,
target_platform=target_platform,
dev_root=dev_root,
game=game,
project_path=project_path,
asset_type=asset_type,
warning_on_missing_assets=warning_on_missing_assets,
layout_target=layout_target,
@@ -527,12 +522,11 @@ def sync_layout_pak(dev_root, target_platform, game, asset_type, warning_on_miss
copy=copy)
def sync_layout_loose(dev_root, target_platform, game, asset_type, warning_on_missing_assets, layout_target,
def sync_layout_loose(target_platform, project_path, asset_type, warning_on_missing_assets, layout_target,
override_pak_folder, copy):
sync_layout_non_vfs(mode=ASSET_MODE_LOOSE,
target_platform=target_platform,
dev_root=dev_root,
game=game,
project_path=project_path,
asset_type=asset_type,
warning_on_missing_assets=warning_on_missing_assets,
layout_target=layout_target,
@@ -548,13 +542,10 @@ ASSET_SYNC_MODE_FUNCTION = {
def main(args):
parser = argparse.ArgumentParser(description="Synchronize a game's assets to a layout folder")
parser = argparse.ArgumentParser(description="Synchronize a project's assets to a layout folder")
parser.add_argument('--dev-root',
help='The path to the dev root',
required=True)
parser.add_argument('-g', '--game',
help='Name of the game whose assets we will sync.',
parser.add_argument('--project-path',
help='The project path whose assets we will sync.',
required=True)
parser.add_argument('-p', '--platform',
help='Target platform for the layout.',
@@ -567,7 +558,7 @@ def main(args):
help='Enable debug logs.')
parser.add_argument('--warn-on-missing-assets',
action='store_true',
help='If the game does not have any built assets, warn rather than return an error')
help='If the project does not have any built assets, warn rather than return an error')
parser.add_argument('-m', '--mode',
type=str,
choices=ALL_ASSET_MODES,
@@ -582,7 +573,7 @@ def main(args):
parser.add_argument('--override-pak-folder',
default='',
help='(optional) If provided, use this path as the path to the pak folder when creating layouts '
'in PAK mode. Otherwise, use the dev-root/pak/${game}_${asset_type}_pak as the source pak folder')
'in PAK mode. Otherwise, use the {project_path}/pak/${project}_${asset_type}_pak as the source pak folder')
parser.add_argument('--build-config',
default='',
help='(optional) If provided, will adjust the asset mode if the provided build-config is "release"')
@@ -599,11 +590,36 @@ def main(args):
parsed_args = parser.parse_args(args)
# Validate the dev_root exists
if not os.path.exists(parsed_args.dev_root):
raise common.LmbrCmdError("Invalid dev root folder. '{}' does not exist".format(parsed_args.dev_root),
# Prepare the logging
logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG if parsed_args.debug else logging.INFO)
# Validate the asset mode
input_asset_mode = parsed_args.mode.upper()
if input_asset_mode not in ALL_ASSET_MODES:
raise common.LmbrCmdError("Invalid asset mode '{}'. Must be one of : '{}'.".format(input_asset_mode, ','.join(ALL_ASSET_MODES)),
common.ERROR_CODE_INVALID_PARAMETER)
# Check if the build config is set, if so, check if its release
build_config = parsed_args.build_config.upper()
if build_config in PAK_ONLY_BUILD_CONFIGS:
input_asset_mode = ASSET_MODE_PAK
logging.info("Starting (%s) Asset Synchronization in %s mode and project %s", parsed_args.asset_type, input_asset_mode, parsed_args.project_path)
start_time = timeit.default_timer()
ASSET_SYNC_MODE_FUNCTION[input_asset_mode](target_platform=parsed_args.platform,
project_path=parsed_args.project_path,
asset_type=parsed_args.asset_type,
warning_on_missing_assets=parsed_args.warn_on_missing_assets,
layout_target=os.path.normpath(parsed_args.layout_root),
override_pak_folder=parsed_args.override_pak_folder,
copy=parsed_args.copy)
duration = timeit.default_timer() - start_time
logging.info("Asset Synchronization complete {:.2f} seconds".format(duration))
# Remove broken symlinks/junctions to the layout folder
if os.path.isdir(parsed_args.layout_root) and not os.path.exists(parsed_args.layout_root):
remove_link(parsed_args.layout_root)
if not os.path.isdir(parsed_args.layout_root):
# If the layout target doesnt exist, check if we want to create it
if parsed_args.create_layout_root:
@@ -617,42 +633,10 @@ def main(args):
raise common.LmbrCmdError("Invalid layout folder (--layout-root): '{}'".format(parsed_args.layout_root),
common.ERROR_CODE_ERROR_DIRECTORY)
# Prepare the logging
logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG if parsed_args.debug else logging.INFO)
# Validate the dev root
for check_file in ('bootstrap.cfg', 'engine.json'):
if not os.path.isfile(os.path.join(parsed_args.dev_root, check_file)):
raise common.LmbrCmdError("Invalid value for --dev-root. Path '{}' missing file '{}'".format(parsed_args.dev_root, check_file),
common.ERROR_CODE_INVALID_PARAMETER)
# Validate the asset mode
input_asset_mode = parsed_args.mode.upper()
if input_asset_mode not in ALL_ASSET_MODES:
raise common.LmbrCmdError("Invalid asset mode '{}'. Must be one of : '{}'.".format(input_asset_mode, ','.join(ALL_ASSET_MODES)),
common.ERROR_CODE_INVALID_PARAMETER)
# Check if the build config is set, if so, check if its release
build_config = parsed_args.build_config.upper()
if build_config in PAK_ONLY_BUILD_CONFIGS:
input_asset_mode = ASSET_MODE_PAK
logging.info("Starting (%s) Asset Synchronization in %s mode and game %s", parsed_args.asset_type, input_asset_mode, parsed_args.game)
start_time = timeit.default_timer()
ASSET_SYNC_MODE_FUNCTION[input_asset_mode](dev_root=os.path.normpath(parsed_args.dev_root),
target_platform=parsed_args.platform,
game=parsed_args.game,
asset_type=parsed_args.asset_type,
warning_on_missing_assets=parsed_args.warn_on_missing_assets,
layout_target=os.path.normpath(parsed_args.layout_root),
override_pak_folder=parsed_args.override_pak_folder,
copy=parsed_args.copy)
duration = timeit.default_timer() - start_time
logging.info("Asset Synchronization complete {:.2f} seconds".format(duration))
if parsed_args.verify:
warnings = verify_layout(layout_dir=os.path.normpath(parsed_args.layout_root),
platform_name=parsed_args.platform,
game_name=parsed_args.game,
project_path=parsed_args.project_path,
asset_mode=input_asset_mode,
asset_type=parsed_args.asset_type)
if warnings > 0:
+18 -52
View File
@@ -24,7 +24,7 @@ from . import common
pytest.param({'fake': 'foo'}, True, id="TestSuccess"),
pytest.param(None, False, id="TestFail")
])
def test_determine_dev_root(tmpdir, engine_json_content, expected_success):
def test_determine_engine_root(tmpdir, engine_json_content, expected_success):
test_folder_heirarchy = 'dev/foo1/foo2/foo3/'
tmpdir.ensure(test_folder_heirarchy)
@@ -40,7 +40,7 @@ def test_determine_dev_root(tmpdir, engine_json_content, expected_success):
expected_path = None
starting_path = str(tmpdir.join(test_folder_heirarchy).realpath())
result = common.determine_dev_root(starting_path)
result = common.determine_engine_root(starting_path)
if expected_path:
assert os.path.normcase(result) == os.path.normcase(expected_path)
@@ -49,7 +49,7 @@ def test_determine_dev_root(tmpdir, engine_json_content, expected_success):
TEST_BOOTSTRAP_CONTENT_1 = """
sys_game_folder = Game1
project_path = Game1
foo = bar
key1 = value1
key2 = value2
@@ -58,7 +58,7 @@ assets = pc
"""
TEST_BOOTSTRAP_CONTENT_2 = """
sys_game_folder = Game2
project_path = Game2
foo = bar
#-------------------------
key1 = value1
@@ -70,12 +70,12 @@ assets = pc
@pytest.mark.parametrize(
"contents, input_keys, expected_result_map", [
pytest.param(TEST_BOOTSTRAP_CONTENT_1, ['sys_game_folder', 'foo', 'assets'], {'sys_game_folder': 'Game1',
pytest.param(TEST_BOOTSTRAP_CONTENT_1, ['project_path', 'foo', 'assets'], {'project_path': 'Game1',
'foo': 'bar',
'assets': 'pc'}, id="TestFullMatch"),
pytest.param(TEST_BOOTSTRAP_CONTENT_2, ['sys_game_folder', 'foo', 'barnone'], {'sys_game_folder': 'Game2',
pytest.param(TEST_BOOTSTRAP_CONTENT_2, ['project_path', 'foo', 'barnone'], {'project_path': 'Game2',
'foo': 'bar'}, id="TestPartialMatch"),
pytest.param(TEST_BOOTSTRAP_CONTENT_2, ['sys_game_foldernone', 'foonone', 'barnone'], {}, id="TestNoMatch")
pytest.param(TEST_BOOTSTRAP_CONTENT_2, ['project_pathnone', 'foonone', 'barnone'], {}, id="TestNoMatch")
]
)
def test_get_bootstrap_values_success(tmpdir, contents, input_keys, expected_result_map):
@@ -223,9 +223,9 @@ subjectB = ${subject_B_value}
TEST_GAME_PROJECT_JSON_FORMAT = """
{{
"project_name": "{game_name}",
"product_name": "{game_name}",
"executable_name": "{game_name}.GameLauncher",
"project_name": "{project_name}",
"product_name": "{project_name}",
"executable_name": "{project_name}.GameLauncher",
"modules" : [],
"project_id": "{{4F3363D3-4A7C-47A6-B464-B21524771358}}",
@@ -244,7 +244,7 @@ def test_verify_game_project_and_dev_root_success(tmpdir):
dev_root = 'dev'
game_name = 'MyFoo'
game_folder = 'myfoo'
game_project_json = TEST_GAME_PROJECT_JSON_FORMAT.format(game_name=game_name)
game_project_json = TEST_GAME_PROJECT_JSON_FORMAT.format(project_name=game_name)
tmpdir.ensure(f'{dev_root}/bootstrap.cfg')
tmpdir.ensure(f'{dev_root}/{game_folder}/project.json')
project_json_path = tmpdir / dev_root / game_folder / 'project.json'
@@ -285,7 +285,7 @@ asset_deploy_type={test_asset_deploy_type}
assert result.asset_deploy_type == test_asset_deploy_type
def test_transform_bootstrap_sysgamefolder(tmpdir):
def test_transform_bootstrap_project_path(tmpdir):
tmpdir.ensure('bootstrap.cfg')
@@ -293,7 +293,7 @@ def test_transform_bootstrap_sysgamefolder(tmpdir):
-- Blah Blah
-- Blah Blah
sys_game_folder=OldProject
project_path=OldProject
-- remote_filesystem - enable Virtual File System (VFS)
-- This feature allows a remote instance of the game to run off assets
@@ -307,53 +307,19 @@ remote_filesystem=0
test_dst_bootstrap = tmpdir / 'bootstrap.transformed.cfg'
test_game_name = 'FooBar'
common.transform_bootstrap_for_game(game_name=test_game_name,
common.transform_bootstrap_for_project(game_name=test_game_name,
src_bootstrap=str(test_src_bootstrap),
dst_bootstrap=str(test_dst_bootstrap))
transformed_text = test_dst_bootstrap.read_text('ascii')
search_gamename = re.search(r"sys_game_folder\s*=\s*(.*)", transformed_text)
search_gamename = re.search(r"project_path\s*=\s*(.*)", transformed_text)
assert search_gamename
assert search_gamename.group(1)
assert search_gamename.group(1) == test_game_name
def test_transform_bootstrap_sysgamename(tmpdir):
tmpdir.ensure('bootstrap.cfg')
test_bootstrap_content = """
-- Blah Blah
-- Blah Blah
sys_game_name=OldProject
-- remote_filesystem - enable Virtual File System (VFS)
-- This feature allows a remote instance of the game to run off assets
-- on the asset processor computers cache instead of deploying them the remote device
-- By default it is off and can be overridden for any platform
remote_filesystem=0
"""
test_src_bootstrap = tmpdir / 'bootstrap.cfg'
test_src_bootstrap.write_text(test_bootstrap_content, encoding='ascii')
test_dst_bootstrap = tmpdir / 'bootstrap.transformed.cfg'
test_game_name = 'FooBar'
common.transform_bootstrap_for_game(game_name=test_game_name,
src_bootstrap=str(test_src_bootstrap),
dst_bootstrap=str(test_dst_bootstrap))
transformed_text = test_dst_bootstrap.read_text('ascii')
search_gamename = re.search(r"sys_game_name\s*=\s*(.*)", transformed_text)
assert search_gamename
assert search_gamename.group(1)
assert search_gamename.group(1) == test_game_name
def test_transform_bootstrap_sysgamefolder_missing(tmpdir):
def test_transform_bootstrap_project_path_missing(tmpdir):
tmpdir.ensure('bootstrap.cfg')
@@ -373,13 +339,13 @@ remote_filesystem=0
test_dst_bootstrap = tmpdir / 'bootstrap.transformed.cfg'
test_game_name = 'FooBar'
common.transform_bootstrap_for_game(game_name=test_game_name,
common.transform_bootstrap_for_project(game_name=test_game_name,
src_bootstrap=str(test_src_bootstrap),
dst_bootstrap=str(test_dst_bootstrap))
transformed_text = test_dst_bootstrap.read_text('ascii')
search_gamename = re.search(r"sys_game_folder\s*=\s*(.*)", transformed_text)
search_gamename = re.search(r"project_path\s*=\s*(.*)", transformed_text)
assert search_gamename
assert search_gamename.group(1)
assert search_gamename.group(1) == test_game_name
+5 -5
View File
@@ -15,35 +15,35 @@ import pytest
from . import current_project
TEST_BOOTSTRAP_CONTENT_1 = """
sys_game_folder = Game1
project_path = Game1
foo = bar
key1 = value1
key2 = value2
assets = pc
"""
TEST_BOOTSTRAP_CONTENT_2 = """
sys_game_folder=Game1
project_path=Game1
foo = bar
key1 = value1
key2 = value2
assets = pc
"""
TEST_BOOTSTRAP_CONTENT_3 = """
sys_game_folder= Game1
project_path= Game1
foo = bar
key1 = value1
key2 = value2
assets = pc
"""
TEST_BOOTSTRAP_CONTENT_4 = """
sys_game_folder =Game1
project_path =Game1
foo = bar
key1 = value1
key2 = value2
assets = pc
"""
TEST_BOOTSTRAP_CONTENT_5 = """
sys_game_folder = Game1
project_path = Game1
foo = bar
key1 = value1
key2 = value2
+39 -57
View File
@@ -37,9 +37,9 @@ def test_copy_asset_files_to_layout_success():
try:
# Setup test vectors
# Blacklisted files, should not show up in the result
test_blacklisted_file = [
'assetprocessorplatformconfig.ini'
# Denied files, should not show up in the result
test_denylist_file = [
'assetprocessorplatformconfig.setreg'
]
# System files that are not the same platform, so should skip
test_skip_system_files = [
@@ -69,18 +69,14 @@ def test_copy_asset_files_to_layout_success():
'good_src_2'
]
test_expected_copied_files = test_dest_diff_as_src + test_src_not_in_dst
test_dev_root = 'dev'
test_game = 'game1'
test_asset_type = 'pc'
test_game_asset_folder = 'game_cache'
test_layout_target = 'layout_target'
test_platform = 'goodplatform'
test_asset_mode = layout_tool.ASSET_MODE_LOOSE
def _mock_os_listdir(path):
assert path == test_game_asset_folder
mock_files = test_blacklisted_file + \
mock_files = test_denylist_file + \
test_skip_system_files + \
test_skip_source_folders + \
test_skip_dest_is_folder + \
@@ -130,8 +126,7 @@ def test_copy_asset_files_to_layout_success():
result_copy_files.append(basename)
shutil.copy2 = _mock_shutil_copy2
layout_tool.copy_asset_files_to_layout(game_name=test_game,
game_asset_folder=test_game_asset_folder,
layout_tool.copy_asset_files_to_layout(project_asset_folder=test_game_asset_folder,
target_platform=test_platform,
layout_target=test_layout_target)
@@ -217,17 +212,16 @@ def test_create_link_error():
@pytest.mark.parametrize(
"game_name, asset_type, ensure_path, warn_on_missing, expected_result", [
pytest.param('Foo', 'pc', 'dev/Cache/Foo/pc/bootstrap.cfg', False, 'dev/Cache/Foo/pc'),
"project_path, asset_type, ensure_path, warn_on_missing, expected_result", [
pytest.param('Foo', 'pc', 'Foo/Cache/pc/bootstrap.cfg', False, 'Foo/Cache/pc'),
pytest.param('Foo', 'pc', 'dev/bootstrap.cfg', True, None),
pytest.param('Foo', 'pc', 'dev/Cache/Foo/es3/bootstrap.cfg', True, None),
pytest.param('Foo', 'pc', 'Foo/Cache/es3/bootstrap.cfg', True, None),
pytest.param('Foo', 'pc', 'dev/bootstrap.cfg', False, common.LmbrCmdError),
pytest.param('Foo', 'pc', 'dev/Cache/Foo/es3/bootstrap.cfg', False, common.LmbrCmdError),
pytest.param('Foo', 'pc', 'Foo/Cache/es3/bootstrap.cfg', False, common.LmbrCmdError),
]
)
def test_construct_and_validate_cache_game_asset_folder_success(tmpdir, game_name, asset_type, ensure_path, warn_on_missing, expected_result):
def test_construct_and_validate_cache_game_asset_folder_success(tmpdir, project_path, asset_type, ensure_path, warn_on_missing, expected_result):
tmpdir.ensure(ensure_path)
dev_root_realpath = str(tmpdir.join('dev').realpath())
if isinstance(expected_result, str):
expected_path_realpath = str(tmpdir.join(expected_result).realpath())
elif expected_result == common.LmbrCmdError:
@@ -236,10 +230,9 @@ def test_construct_and_validate_cache_game_asset_folder_success(tmpdir, game_nam
expected_path_realpath = None
try:
result = layout_tool.construct_and_validate_cache_game_asset_folder(dev_root=dev_root_realpath,
game_name=game_name,
result = layout_tool.construct_and_validate_cache_project_asset_folder(project_path=project_path,
asset_type=asset_type,
warn_on_missing_game_cache=warn_on_missing)
warn_on_missing_project_cache=warn_on_missing)
assert expected_result != common.LmbrCmdError, "Expecting an error result"
if result == None:
@@ -273,31 +266,22 @@ def test_sync_layout_vfs_success(tmpdir, existing_temp_vfs_folder, existing_gems
try:
# Simple Test Parameters
test_dev_root = str(tmpdir.join('dev').realpath())
test_game = 'Foo'
test_engine_root = str(tmpdir.join('engine-root').realpath())
test_project_path = str(tmpdir.join('Foo').realpath())
test_project_name_lower = 'foo'
test_target_platform = 'bogus'
test_asset_type = 'pc'
game_folder = test_game.lower()
# Setup a test dev and game cache folder structure inside the temp folder
path_to_src_cache = 'dev/Cache/{}/pc'.format(test_game)
path_to_src_cache_config = '{}/{}/config'.format(path_to_src_cache, test_game.lower())
path_to_src_cache_config_file = '{}/game.xml'.format(path_to_src_cache_config)
tmpdir.ensure(path_to_src_cache_config_file)
# Make a dummy config file
config_file = tmpdir.join(path_to_src_cache_config_file)
config_file.write('<foo></foo>')
# Capture relevant real paths in the temp folder so we can verify our assertions
cache_game_folder = os.path.join(test_dev_root, 'Cache', test_game)
cache_game_folder = os.path.join(test_project_path, 'Cache')
cache_game_folder_gems = os.path.join(cache_game_folder, test_asset_type, 'gems')
path_to_src_config_realpath = str(tmpdir.join(path_to_src_cache_config).realpath())
layout_target_root_realpath = str(tmpdir.join('layout').realpath())
layout_target_gems_realpath = os.path.join(layout_target_root_realpath, 'gems')
layout_target_game_realpath = os.path.join(layout_target_root_realpath, test_game)
layout_target_game_realpath = os.path.join(layout_target_root_realpath)
# If we are optionally testing existing links in a layout folder, track the expected and actual rmdirs
actual_rmdir_paths = set()
@@ -331,8 +315,7 @@ def test_sync_layout_vfs_success(tmpdir, existing_temp_vfs_folder, existing_gems
# Predict the temp folder name
hasher = hashlib.md5()
hasher.update(test_dev_root.encode('UTF-8'))
hasher.update(game_folder.encode('UTF-8'))
hasher.update(test_project_path.encode('UTF-8'))
result = hasher.hexdigest()
tmp_folder_subfolder = 'ly-layout-{}'.format(result)
test_layout_folder = str(tmpdir.join('{}/vfs/foo'.format(tmp_folder_subfolder)).realpath())
@@ -347,7 +330,6 @@ def test_sync_layout_vfs_success(tmpdir, existing_temp_vfs_folder, existing_gems
os.rmdir = _mock_os_rmdir
mock_layout_tool_create_link_validation = {
os.path.normcase(path_to_src_config_realpath): os.path.normcase(test_layout_config_folder),
os.path.normcase(cache_game_folder_gems): os.path.normcase(layout_target_gems_realpath),
os.path.normcase(test_layout_folder): os.path.normcase(layout_target_game_realpath)
}
@@ -360,15 +342,14 @@ def test_sync_layout_vfs_success(tmpdir, existing_temp_vfs_folder, existing_gems
layout_tool.create_link = _mock_layout_tool_create_link
def _mock_copy_asset_files_to_layout(game_name, game_asset_folder, target_platform, layout_target):
def _mock_copy_asset_files_to_layout(project_path, project_asset_folder, target_platform, layout_target):
# Validate the correct call to copy asset files
assert target_platform == target_platform
assert os.path.normcase(layout_target) == os.path.normcase(layout_target_root_realpath)
layout_tool.copy_asset_files_to_layout = _mock_copy_asset_files_to_layout
layout_tool.sync_layout_vfs(dev_root = test_dev_root,
target_platform = test_target_platform,
game = test_game,
layout_tool.sync_layout_vfs(target_platform = test_target_platform,
project_path = test_project_path,
asset_type = test_asset_type,
warning_on_missing_assets = False,
layout_target = layout_target_root_realpath,
@@ -404,19 +385,19 @@ def test_sync_layout_non_vfs_success(tmpdir, mode, existing_game_link, existing_
old_remove_link = layout_tool.remove_link
try:
# Simple Test Parameters
tmpdir.ensure('dev/bootstrap.cfg')
dev_root_realpath = str(tmpdir.join('dev').realpath())
test_game = 'Foo'
tmpdir.ensure('engine-root/bootstrap.cfg')
engine_root_realpath = str(tmpdir.join('engine-root').realpath())
test_project_path = str(tmpdir.join('Foo').realpath())
test_project_name_lower = 'foo'
test_target_platform = 'bogus'
test_asset_type = 'pc'
game_folder = test_game.lower()
cache_game_folder_realpath = os.path.join(dev_root_realpath, 'Cache', test_game)
cache_game_folder_realpath = os.path.join(test_project_path, 'Cache')
# Make sure a dummy layout folder is created
tmpdir.ensure('layout/dummy.txt')
test_layout_target_realpath = str(tmpdir.join('layout').realpath())
test_layout_target_gems_realpath = os.path.join(test_layout_target_realpath, 'gems')
test_layout_target_game_realpath = os.path.join(test_layout_target_realpath, game_folder)
test_layout_target_game_realpath = os.path.join(test_layout_target_realpath,)
# If we are optionally testing existing links in a layout folder, track the expected and actual rmdirs
actual_rmdir_paths = set()
@@ -441,11 +422,13 @@ def test_sync_layout_non_vfs_success(tmpdir, mode, existing_game_link, existing_
if mode == 'PAK':
# In PAK Mode, the linking rules are slightly different. The 'game folder' link points to inside the pak folder, and there is no 'gems' link
if test_override_pak_folder:
test_game_asset_folder = os.path.join(dev_root_realpath, test_override_pak_folder, '{}_{}_paks'.format(game_folder, test_asset_type))
cache_game_folder_game_realpath = os.path.join(test_game_asset_folder, game_folder)
test_game_asset_folder = os.path.join(engine_root_realpath, test_override_pak_folder,
f'{test_project_name_lower}_{test_asset_type}_paks')
cache_game_folder_game_realpath = os.path.join(test_game_asset_folder)
else:
test_game_asset_folder = os.path.join(dev_root_realpath, 'Pak', '{}_{}_paks'.format(game_folder, test_asset_type))
cache_game_folder_game_realpath = os.path.join(test_game_asset_folder, game_folder)
test_game_asset_folder = os.path.join(engine_root_realpath, 'Pak',
f'{test_project_name_lower}_{test_asset_type}_paks')
cache_game_folder_game_realpath = os.path.join(test_game_asset_folder)
mock_layout_tool_create_link_validation[os.path.normcase(cache_game_folder_game_realpath)] = os.path.normcase(test_layout_target_game_realpath)
@@ -460,7 +443,7 @@ def test_sync_layout_non_vfs_success(tmpdir, mode, existing_game_link, existing_
test_game_asset_folder = os.path.join(cache_game_folder_realpath, test_asset_type)
cache_game_folder_gems_realpath = os.path.join(cache_game_folder_realpath, test_asset_type, 'gems')
cache_game_folder_game_realpath = os.path.join(cache_game_folder_realpath, test_asset_type, game_folder)
cache_game_folder_game_realpath = os.path.join(cache_game_folder_realpath, test_asset_type)
mock_layout_tool_create_link_validation[os.path.normcase(cache_game_folder_gems_realpath)] = os.path.normcase(test_layout_target_gems_realpath)
mock_layout_tool_create_link_validation[os.path.normcase(cache_game_folder_game_realpath)] = os.path.normcase(test_layout_target_game_realpath)
@@ -468,8 +451,8 @@ def test_sync_layout_non_vfs_success(tmpdir, mode, existing_game_link, existing_
assert False, "Invalid Mode {}".format(mode)
os.makedirs(test_game_asset_folder, exist_ok=True)
def _mock_copy_asset_files_to_layout(game_name, game_asset_folder, target_platform, layout_target):
assert os.path.normcase(game_asset_folder) == os.path.normcase(test_game_asset_folder)
def _mock_copy_asset_files_to_layout(project_path, project_asset_folder, target_platform, layout_target):
assert os.path.normcase(project_asset_folder) == os.path.normcase(test_game_asset_folder)
assert target_platform == test_target_platform
assert layout_target == test_layout_target_realpath
layout_tool.copy_asset_files_to_layout = _mock_copy_asset_files_to_layout
@@ -483,8 +466,7 @@ def test_sync_layout_non_vfs_success(tmpdir, mode, existing_game_link, existing_
layout_tool.sync_layout_non_vfs(mode = mode,
target_platform = test_target_platform,
dev_root = dev_root_realpath,
game = test_game,
project_path = test_project_path,
asset_type = test_asset_type,
warning_on_missing_assets = False,
layout_target = test_layout_target_realpath,