Initial commit

This commit is contained in:
alexpete
2021-03-05 11:26:34 -08:00
commit a10351f38d
27091 changed files with 5521199 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
@@ -0,0 +1,552 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
import datetime
import logging
import os
import json
import platform
import subprocess
import sys
import time
import pathlib
from distutils.version import LooseVersion
# 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)
from cmake.Tools import common
from cmake.Tools.Platform.Android import android_support
# The following is the list of known android external storage paths that we will attempt to verify on a device and
# return the first one that is detected
KNOWN_ANDROID_EXTERNAL_STORAGE_PATHS = [
'/sdcard/',
'/storage/emulated/0/',
'/storage/emulated/legacy/',
'/storage/sdcard0/',
'/storage/self/primary/',
]
ANDROID_TARGET_TIMESTAMP_FILENAME = 'deploy.timestamp'
class AndroidDeployment(object):
"""
Class to manage the deployment of game assets to an android device (Separately from the APK)
"""
DEPLOY_APK_ONLY = 'APK'
DEPLOY_ASSETS_ONLY = 'ASSETS'
DEPLOY_BOTH = 'BOTH'
def __init__(self, dev_root, build_dir, configuration, android_device_filter, clean_deploy, android_sdk_path, deployment_type, game_name=None, asset_mode=None, asset_type=None, embedded_assets=True):
"""
Initialize the Android Deployment Worker
:param dev_root: The dev-root of the engine
:param android_device_filter: An optional list of devices to filter on the connected devices to deploy to. If not supplied, deploy to all devices
:param clean_deploy: Option to clean the target device's assets before deploying the game's assets from the host
:param android_sdk_path: Path to the android SDK (to use the adb tool)
:param deployment_type: The type of deployment (DEPLOY_APK_ONLY, DEPLOY_ASSETS_ONLY, or DEPLOY_BOTH)
:param game_name: The name of the game whoses assets are being deployed. None if is_test_project is True
:param asset_mode: The asset mode of deployment (LOOSE, PAK, VFS). None if is_test_project is True
:param asset_type: The asset type (for android, 'es3'). None if is_test_project is True
:param embedded_assets: Boolean to indicate if the assets are embedded in the APK or not
"""
self.dev_root = pathlib.Path(dev_root)
self.build_dir = self.dev_root / build_dir
self.configuration = configuration
self.game_name = game_name
self.asset_mode = asset_mode
self.asset_type = asset_type
self.clean_deploy = clean_deploy
self.embedded_assets = embedded_assets
self.deployment_type = deployment_type
self.is_test_project = game_name == android_support.TEST_RUNNER_PROJECT
if not self.is_test_project:
if embedded_assets:
# If the assets are embedded, then warn that both APK and ASSETS will be deployed even if 'BOTH' is not specified
if deployment_type in (AndroidDeployment.DEPLOY_APK_ONLY, AndroidDeployment.DEPLOY_ASSETS_ONLY):
logging.warning(f"Deployment type of {deployment_type} set but the assets are embedded in the APK. Both the APK and the Assets will be deployed.")
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
assert game_name is not None, f"'game_name' is required"
self.game_name = game_name
assert asset_mode is not None, f"'asset_mode' is required"
self.asset_mode = asset_mode
assert asset_type is not None, f"'asset_type' is required"
self.asset_type = asset_type
self.files_in_asset_path = list(self.local_asset_path.glob('**/*'))
self.android_settings = AndroidDeployment.read_android_settings(self.dev_root, game_name)
else:
self.local_asset_path = None
if asset_mode:
logging.warning(f"'asset_mode' argument '{asset_mode}' ignored for unit test deployment.")
if asset_type:
logging.warning(f"'asset_type' argument '{asset_type}' ignored for unit test deployment.")
self.files_in_asset_path = []
self.apk_path = self.build_dir / 'app' / 'build' / 'outputs' / 'apk' / configuration / f'app-{configuration}.apk'
self.android_device_filter = [android_device.strip() for android_device in android_device_filter.split(',')] if android_device_filter else []
self.adb_path = AndroidDeployment.resolve_adb_tool(pathlib.Path(android_sdk_path))
self.adb_started = False
@staticmethod
def read_android_settings(dev_root, game_name):
"""
Read and parse the project.json file into a dictionary to process the specific attributes needed for the manifest template
:param dev_root: The dev root we are working from
:param game_name: Name of the game under the dev root
:return: The android settings for the game project if any
"""
game_folder = dev_root / game_name
game_folder_project_properties_path = game_folder / 'project.json'
game_project_properties_content = game_folder_project_properties_path.resolve(strict=True)\
.read_text(encoding=common.DEFAULT_TEXT_READ_ENCODING,
errors=common.ENCODING_ERROR_HANDLINGS)
# Extract the key attributes we need to process and build up our environment table
game_project_json = json.loads(game_project_properties_content)
android_settings = game_project_json.get('android_settings', {})
return android_settings
@staticmethod
def resolve_adb_tool(android_sdk_path):
"""
Resolve the location of the adb tool based on the input Android SDK Path
:param android_sdk_path: The android SDK path to search for the adb tool
:return: The absolute path to the adb tool
"""
adb_target = 'adb.exe' if platform.system() == 'Windows' else 'adb'
check_adb_target = android_sdk_path / 'platform-tools' / adb_target
if not check_adb_target.exists():
raise common.LmbrCmdError(f"Invalid Android SDK path '{str(android_sdk_path)}': Unable to locate '{adb_target}'.")
return check_adb_target
def get_android_project_settings(self, key_name, default_value):
return self.android_settings.get(key_name, default_value)
def adb_call(self, arg_list, device_id=None):
"""
Wrapper to execute the adb command-line tool
:param arg_list: Argument list to send to the tool
:param device_id: Optional device id (from the 'get_target_android_devices' call) to invoke the call to.
:return: The stdout result of the call
"""
if isinstance(arg_list, str):
arg_list = [arg_list]
call_arguments = [str(self.adb_path.resolve())]
if device_id:
call_arguments.extend(['-s', device_id])
call_arguments.extend(arg_list)
output = subprocess.check_output(call_arguments,
shell=True,
stderr=subprocess.DEVNULL).decode(common.DEFAULT_TEXT_READ_ENCODING,
common.ENCODING_ERROR_HANDLINGS)
return output
def adb_shell(self, command, device_id):
"""
Special wrapper around calling "adb shell" which will invoke a shell command on the android device
:param command: The shell command to invoke on the android device
:param device_id: The device id (from the 'get_target_android_devices' call) to invoke the shell call on
:return: The stdout result of the call
"""
shell_command = ['shell', command]
return self.adb_call(shell_command, device_id=device_id)
def adb_ls(self, path, device_id, args=None):
"""
Request an 'ls' call on the android device
:param path: The path to perform the 'ls' call on
:param device_id: device id (from the 'get_target_android_devices' call) to invoke the shell call on
:param args: Additional args to pass into the l'ls' call
:return: Tuple of Boolean result of the call and the output of the call
"""
error_messages = [
'No such file or directory',
'Permission denied'
]
shell_command = ['ls']
if args:
shell_command.extend(args)
shell_command.append(path)
logging.debug(f"Testing {device_id}: ls {' '.join(shell_command)}")
raw_output = self.adb_shell(command=' '.join(shell_command),
device_id=device_id)
if not raw_output:
logging.debug('adb_ls: No output given')
return False, None
if raw_output is None or any([error for error in error_messages if error in raw_output]):
logging.debug('adb_ls: Error message found')
status = False
else:
logging.debug('adb_ls: Command was successful')
status = True
return status, raw_output
def get_target_android_devices(self):
"""
Gets all of the connected android devices with adb, filtered by the set optional device filter
:return: list of serial numbers of optionally filtered connected devices.
"""
connected_devices = []
# Call adb to get the device list and process the raw response
raw_devices_output = self.adb_call("devices")
if not raw_devices_output:
raise common.LmbrCmdError("Error getting connected devices through adb")
device_output_list = raw_devices_output.split(os.linesep)
for device_output in device_output_list:
if any(x in device_output for x in ['List', '*']):
logging.debug(f"Skipping the following line as it has 'List', '*' or 'emulator' in it: {device_output}")
continue
device_serial = device_output.split()
if device_serial:
if 'unauthorized' in device_output.lower():
logging.warning(f"Device {device_serial[0]} is not authorized for development access. Please reconnect the device and check for a confirmation dialog.")
elif device_serial[0] in self.android_device_filter or not self.android_device_filter:
connected_devices.append(device_serial[0])
else:
logging.debug(f"Skipping filtered out Device {device_serial[0]} .")
if not connected_devices:
raise common.LmbrCmdError("No connected android devices found")
return connected_devices
def check_known_android_paths(self, device_id):
"""
Look for a known android path that is writeable and return the first one that is found
:param device_id: The device id (from the 'get_target_android_devices' call) to invoke the shell call on
:return: The first available android path if found, None if not
"""
for path in KNOWN_ANDROID_EXTERNAL_STORAGE_PATHS:
logging.debug(f"Checking known path '{path}' on device '{device_id}'")
# Test the path by performing an 'ls' call on it and checking if an error is returned from the result
result, output = self.adb_ls(path=path,
args=None,
device_id=device_id)
if result:
return path[:-1]
return None
def detect_device_storage_path(self, device_id):
"""
Uses the device's environment variable "EXTERNAL_STORAGE" to determine the correct
path to public storage that has write permissions. If at any point does the env var
validation fail, fallback to checking known possible paths to external storage.
:param device_id:
:return: The first available storage device
"""
external_storage = self.adb_shell(command="set | grep EXTERNAL_STORAGE",
device_id=device_id)
if not external_storage:
logging.debug(f"Unable to get 'EXTERNAL_STORAGE' environment from device '{device_id}'. Falling back to known android paths.")
return self.check_known_android_paths(device_id)
# Given the 'EXTERNAL_STORAGE' environment, parse out the value and validate it
storage_path_key_value = external_storage.split('=')
if len(storage_path_key_value) != 2:
logging.debug(f"The value for 'EXTERNAL_STORAGE' environment from device '{device_id}' does not represent a valid key-value pair: {storage_path_key_value}. Falling back to known android paths")
return self.check_known_android_paths(device_id)
# Check the existence and permissions issue of the storage path
storage_path = storage_path_key_value[1].strip()
is_external_valid, _ = self.adb_ls(path=storage_path,
device_id=device_id)
if is_external_valid:
return storage_path
# The set external path has an issue, attempt to determine its real path through an adb shell call
logging.debug(f"The path specified in EXTERNAL_STORAGE seems to have permission issues, attempting to resolve with realpath for device {device_id}.")
real_path = self.adb_shell(command=f'realpath {storage_path}',
device_id=device_id)
if not real_path:
logging.debug(f"Unable to determine the real path '{storage_path}' (from EXTERNAL_STORAGE) for {self.game_name} on device {device_id}. Falling back to known android paths")
return self.check_known_android_paths(device_id)
real_path = real_path.strip()
is_external_valid, _ = self.adb_ls(path=real_path,
device_id=device_id)
if is_external_valid:
return real_path
logging.debug(f'Unable to validate the resolved EXTERNAL_STORAGE environment variable path for device {device_id}.')
return self.check_known_android_paths(device_id)
def get_device_file_timestamp(self, remote_file_path, device_id):
"""
Get the integer timestamp value of a file from a given device.
:param remote_file_path: The path to the timestamp file on the android device
:param device_id: The device id (from the 'get_target_android_devices' call) to invoke the shell call on
:return: The time value if found, None if not
"""
try:
timestamp_string = self.adb_shell(command=f'cat {remote_file_path}',
device_id=device_id).strip()
except (subprocess.CalledProcessError, AttributeError):
return None
if not timestamp_string:
return None
for fmt in ('%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M:%S.%f'):
try:
target_time = time.mktime(time.strptime(timestamp_string, fmt))
break
except ValueError:
target_time = None
return target_time
def update_device_file_timestamp(self, relative_assets_path, device_id):
"""
Update the device timestamp file on an android device to track files that need updating on pushes
:param relative_assets_path: The relative path to the assets on the android device
:param device_id: The device id (from the 'get_target_android_devices' call) to invoke the shell call on
"""
timestamp_str = str(datetime.datetime.now())
logging.debug(f"Updating timestamp on device {device_id} to {timestamp_str}")
local_timestamp_file_path = self.local_asset_path / ANDROID_TARGET_TIMESTAMP_FILENAME
local_timestamp_file_path.write_text(timestamp_str)
target_timestamp_file_path = f'{relative_assets_path}/{ANDROID_TARGET_TIMESTAMP_FILENAME}'
self.adb_call(arg_list=['push', str(local_timestamp_file_path), target_timestamp_file_path],
device_id=device_id)
@staticmethod
def should_copy_file(check_path, check_time):
"""
Check if a source file should be copied, by checking if its timestamp is newer than the 'check_time'
:param check_path: The path to the source file whose timestamp will be evaluated
:param check_time: The baseline 'check_time' value to compare the source file timestamp against
:return: True if the source file is newer than the baseline 'check_time', False if not
"""
if not check_path.is_file():
return False
stat_src = check_path.stat()
should_copy = stat_src.st_mtime >= check_time
return should_copy
def check_package_installed(self, package_name, target_device):
"""
Checks if the package for the game is currently installed or not
@param package_name: The name of the package to search for
@param target_device: The target device to search for the package on
@return: True if there an existing package on the device with the same package name, false if not
"""
output_result = self.adb_call(['shell', 'cmd', 'package', 'list', 'packages', package_name],
target_device)
return output_result != ''
def install_apk_to_device(self, target_device):
"""
Install the APK to a target device
@param target_device: The device id of the connected device to install to
"""
if self.is_test_project:
android_package_name = android_support.TEST_RUNNER_PACKAGE_NAME
else:
android_package_name = self.get_android_project_settings(key_name='package_name',
default_value='com.lumberyard.sdk')
if self.clean_deploy and self.check_package_installed(android_package_name, target_device):
logging.info(f"Device '{target_device}': Uninstalling pre-existing APK for {self.game_name} ...")
self.adb_call(arg_list=['uninstall', android_package_name],
device_id=target_device)
logging.info(f"Device '{target_device}': Installing APK for {self.game_name} ...")
self.adb_call(arg_list=['install', '-t', '-r', str(self.apk_path.resolve())],
device_id=target_device)
def install_assets_to_device(self, detected_storage, target_device):
"""
Install the assets for the game to a target device
@param detected_storage: The detected storage path on the target device
@param target_device: The ID of the target device
"""
assert not self.is_test_project
android_package_name = self.get_android_project_settings(key_name='package_name',
default_value='com.lumberyard.sdk')
relative_assets_path = f'Android/data/{android_package_name}/files'
output_target = f'{detected_storage}/{relative_assets_path}'
device_timestamp_file = f'{output_target}/{ANDROID_TARGET_TIMESTAMP_FILENAME}'
# Track the current timestamp if possible to see if we can incrementally push files rather
# than always pushing all files
target_timestamp = self.get_device_file_timestamp(remote_file_path=device_timestamp_file,
device_id=target_device)
if self.clean_deploy:
logging.info(f"Device '{target_device}': Cleaning target assets before deployment...")
self.adb_shell(command=f'rm -rf {output_target}',
device_id=target_device)
logging.info(f"Device '{target_device}': Target cleaned.")
settings_registry_src = self.build_dir / 'app/src/main/assets/Registry'
settings_registry_dst = f'{output_target}/Registry'
if self.clean_deploy or not target_timestamp:
logging.info(f"Device '{target_device}': Pushing {len(self.files_in_asset_path)} files from {str(self.local_asset_path.resolve())} to device ...")
paths_to_deploy = [(str(self.local_asset_path.resolve()), output_target),
(str(settings_registry_src), settings_registry_dst)]
for path_to_deploy, target_path in paths_to_deploy:
try:
self.adb_call(arg_list=['push', str(path_to_deploy), target_path],
device_id=target_device)
except subprocess.CalledProcessError as err:
# Something went wrong, clean up before leaving
self.adb_shell(command=f'rm -rf {output_target}',
device_id=target_device)
raise err
else:
# If no clean was specified, individually inspect all files to see if it needs to be updated
files_to_copy = []
for asset_file in self.files_in_asset_path:
# TODO: Check if the target exists in the destination as well?
if AndroidDeployment.should_copy_file(asset_file, target_timestamp):
files_to_copy.append(asset_file)
if len(files_to_copy) > 0:
logging.info(f"Copying {len(files_to_copy)} assets to device {target_device}")
for src_path in files_to_copy:
relative_path = os.path.relpath(str(src_path), str(self.local_asset_path)).replace('\\', '/')
target_path = f"{output_target}/{relative_path}"
self.adb_call(arg_list=['push', str(src_path), target_path],
device_id=target_device)
# Always update the settings registry
self.adb_call(arg_list=['push', str(settings_registry_src), settings_registry_dst],
device_id=target_device)
self.update_device_file_timestamp(relative_assets_path=output_target,
device_id=target_device)
def execute(self):
"""
Execute the asset deployment
"""
if self.is_test_project:
if not self.apk_path.is_file():
raise common.LmbrCmdError(f"Missing apk for {android_support.TEST_RUNNER_PROJECT} ({str(self.apk_path)}). Make sure it is built and is set as a signed APK.")
else:
if self.embedded_assets or self.deployment_type in (AndroidDeployment.DEPLOY_APK_ONLY, AndroidDeployment.DEPLOY_BOTH):
if not self.apk_path.is_file():
raise common.LmbrCmdError(f"Missing apk for game {self.game_name} ({str(self.apk_path)}). Make sure it is built and is set as a signed APK.")
if not self.embedded_assets or self.deployment_type in (AndroidDeployment.DEPLOY_ASSETS_ONLY, AndroidDeployment.DEPLOY_BOTH):
if not self.local_asset_path.is_dir():
raise common.LmbrCmdError(f"Missing {self.asset_type} assets for game {self.game_name} .")
try:
logging.debug("Starting ADB Server")
self.adb_call('start-server')
self.adb_started = True
# Get the list of target devices to deploy to
target_devices = self.get_target_android_devices()
if not target_devices:
raise common.LmbrCmdError("No connected and eligible android devices found")
for target_device in target_devices:
detected_storage = self.detect_device_storage_path(target_device)
if not detected_storage:
logging.warning(f"Unable to resolve storage path for device '{target_device}'. Skipping.")
continue
if self.is_test_project:
# If this is the unit test runner, then only install the APK, assets are not applicable
self.install_apk_to_device(target_device=target_device)
else:
# Otherwise install the apk and assets based on the deployment type
if self.embedded_assets or self.deployment_type in (AndroidDeployment.DEPLOY_APK_ONLY, AndroidDeployment.DEPLOY_BOTH):
self.install_apk_to_device(target_device=target_device)
if not self.embedded_assets and self.deployment_type in (AndroidDeployment.DEPLOY_ASSETS_ONLY, AndroidDeployment.DEPLOY_BOTH):
if self.deployment_type == AndroidDeployment.DEPLOY_ASSETS_ONLY:
# If we are deploying assets only without an APK, make sure the APK is even installed first
android_package_name = self.get_android_project_settings(key_name='package_name',
default_value='com.lumberyard.sdk')
if not self.check_package_installed(package_name=android_package_name,
target_device=target_device):
raise common.LmbrCmdError(f"Unable to locate APK for {self.game_name} on device '{target_device}'. Make sure it is installed "
f"first before installing the assets.")
self.install_assets_to_device(detected_storage=detected_storage,
target_device=target_device)
logging.info(f"{self.game_name} deployed to device {target_device}")
finally:
if self.adb_started:
self.adb_call('kill-server')
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,125 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
import argparse
import logging
import os
import pathlib
import sys
# 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)
from cmake.Tools import common
from cmake.Tools.Platform.Android import android_deployment
DEPLOY_TYPES = (android_deployment.AndroidDeployment.DEPLOY_APK_ONLY,
android_deployment.AndroidDeployment.DEPLOY_ASSETS_ONLY,
android_deployment.AndroidDeployment.DEPLOY_BOTH)
def validate_android_deployment_arguments(build_dir_name):
"""
Validate the minimal platform deployment arguments
@param build_dir_name: The name of the build directory relative to the current working directory
@param game_name: The name of the game project to deploy
@return: Tuple of (resolved pathlib, game name, asset mode, asset_type, platform_settings object, Android SDK path, embedded_assets (bool) )
"""
build_dir = pathlib.Path(os.getcwd()) / build_dir_name
if not build_dir.is_dir():
raise common.LmbrCmdError(f"Invalid build directory {build_dir_name}")
platform_settings = common.PlatformSettings(build_dir)
if not platform_settings.projects:
raise common.LmbrCmdError("Missing required platform settings object from build directory.")
game_name = platform_settings.projects[0]
android_sdk_path = getattr(platform_settings, 'android_sdk_path', None)
if not android_sdk_path:
raise common.LmbrCmdError(f"Android SDK Path {android_sdk_path} is missing in the platform settings for {build_dir_name}.")
if not os.path.isdir(android_sdk_path):
raise common.LmbrCmdError(f"Android SDK Path {android_sdk_path} in the platform settings for {build_dir_name} is not valid.")
embedded_assets_str = getattr(platform_settings, 'embed_assets_in_apk', None)
if not embedded_assets_str:
raise common.LmbrCmdError(f"The emdedded assets flag 'embed_assets_in_apk' is missing in the platform settings for {build_dir_name}.")
embedded_assets = embedded_assets_str.lower() in ('t', 'true', '1')
return build_dir, game_name, platform_settings.asset_deploy_mode, platform_settings.asset_deploy_type, android_sdk_path, embedded_assets
def main(args):
parser = argparse.ArgumentParser()
parser.add_argument('-b', '--build-dir',
help='The relative build directory to deploy from.',
required=True)
parser.add_argument('-c', '--configuration',
help='The build configuration from the build directory for the source deployment files',
default='profile')
parser.add_argument('--device-id-filter',
help='Comma separated list of connected android device IDs to filter the deployment to. If not supplied, no filter will be applied and deployment will occur on all devices.',
default='')
parser.add_argument('-t', '--deployment-type',
help=f'The deployment type ({"|".join(DEPLOY_TYPES)}) to execute.',
choices=DEPLOY_TYPES,
default=android_deployment.AndroidDeployment.DEPLOY_BOTH)
parser.add_argument('--clean',
help='Option to clean the target dev kit before deploying, ensuring a clean installation.',
action='store_true')
parser.add_argument('--debug',
help='Option to enable debug messages',
action='store_true')
parsed_args = parser.parse_args(args)
# Prepare the logging
logging.basicConfig(format='%(levelname)s: %(message)s',
level=logging.DEBUG if parsed_args.debug else logging.INFO)
build_dir, game_project, asset_mode, asset_type, android_sdk_path, embedded_assets = validate_android_deployment_arguments(build_dir_name=parsed_args.build_dir)
deployment = android_deployment.AndroidDeployment(dev_root=ROOT_DEV_PATH,
build_dir=build_dir,
configuration=parsed_args.configuration,
game_name=game_project,
asset_mode=asset_mode,
asset_type=asset_type,
embedded_assets=embedded_assets,
android_device_filter=parsed_args.device_id_filter,
clean_deploy=parsed_args.clean,
android_sdk_path=android_sdk_path,
deployment_type=parsed_args.deployment_type)
deployment.execute()
if __name__ == '__main__':
try:
main(sys.argv[1:])
exit(0)
except common.LmbrCmdError as err:
print(str(err), file=sys.stderr)
exit(err.code)
@@ -0,0 +1,330 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
import argparse
import logging
import os
import pathlib
import platform
import re
import sys
from distutils.version import LooseVersion
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)
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')
GRADLE_VERSION_REGEX = re.compile(r"Gradle\s(\d+.\d+.?\d*)")
GRADLE_EXECUTABLE = 'gradle.bat' if platform.system() == 'Windows' else 'gradle'
def verify_gradle(override_gradle_path=None):
"""
Verify the installed gradle requirement.
"""
return common.verify_tool(override_tool_path=override_gradle_path,
tool_name='gradle',
tool_filename=GRADLE_EXECUTABLE,
argument_name=GRADLE_ARGUMENT_NAME,
tool_version_argument='-v',
tool_version_regex=GRADLE_VERSION_REGEX,
min_version=GRADLE_MIN_VERSION,
max_version=GRADLE_MAX_VERSION)
CMAKE_ARGUMENT_NAME = '--cmake-install-path'
CMAKE_MIN_VERSION = LooseVersion('3.17.0')
CMAKE_VERSION_REGEX = re.compile(r'cmake version (\d+.\d+.?\d*)')
CMAKE_EXECUTABLE = 'cmake.exe' if platform.system() == 'Windows' else 'cmake'
def verify_cmake(override_cmake_path=None):
"""
Verify the installed cmake requirement.
"""
return common.verify_tool(override_tool_path=override_cmake_path,
tool_name='cmake',
tool_filename=CMAKE_EXECUTABLE,
argument_name=CMAKE_ARGUMENT_NAME,
tool_version_argument='--version',
tool_version_regex=CMAKE_VERSION_REGEX,
min_version=CMAKE_MIN_VERSION,
max_version=None)
NINJA_ARGUMENT_NAME = '--ninja-install-path'
NINJA_VERSION_REGEX = re.compile(r'(\d+.\d+.?\d*)')
NINJA_EXECUTABLE = 'ninja.exe' if platform.system() == 'Windows' else 'ninja'
def verify_ninja(override_ninja_path=None):
"""
Verify the installed ninja requirement.
"""
return common.verify_tool(override_tool_path=override_ninja_path,
tool_name='ninja',
tool_filename='ninja.exe' if platform.system() == 'Windows' else 'ninja',
argument_name=NINJA_ARGUMENT_NAME,
tool_version_argument='--version',
tool_version_regex=NINJA_VERSION_REGEX,
min_version=None,
max_version=None)
SIGNING_PROFILE_STORE_FILE_ARGUMENT_NAME = "--signconfig-store-file"
SIGNING_PROFILE_STORE_PASSWORD_ARGUMENT_NAME = "--signconfig-store-password"
SIGNING_PROFILE_KEY_ALIAS_ARGUMENT_NAME = "--signconfig-key-alias"
SIGNING_PROFILE_KEY_PASSWORD_ARGUMENT_NAME = "--signconfig-key-password"
def build_optional_signing_profile(store_file, store_password, key_alias, key_password):
# If none of the arguments are set, then return None and skip the signing config generation
if not any([store_file, store_password, key_alias, key_password]):
return
return android_support.AndroidSigningConfig(store_file=store_file,
store_password=store_password,
key_alias=key_alias,
key_password=key_password)
ANDROID_SDK_ARGUMENT_NAME = '--android-sdk-path'
ANDROID_SDK_PLATFORM_ARGUMENT_NAME = '--android-sdk-version'
ANDROID_SDK_PREFERRED_TOOL_VER = '--android-sdk-build-tool-version'
ANDROID_NDK_ARGUMENT_NAME = '--android-ndk-path'
ANDROID_NDK_PLATFORM_ARGUMENT_NAME = '--android-ndk-version'
# Constants for asset-related options for APK generation
INCLUDE_APK_ASSETS_ARGUMENT_NAME = "--include-apk-assets"
ASSET_MODE_ARGUMENT_NAME = "--asset-mode"
ASSET_MODE_PAK = 'PAK'
ASSET_MODE_LOOSE = 'LOOSE'
ASSET_MODE_VFS = 'VFS'
ALL_ASSET_MODES = [ASSET_MODE_PAK, ASSET_MODE_LOOSE, ASSET_MODE_VFS]
ASSET_TYPE_ARGUMENT_NAME = '--asset-type'
DEFAULT_ASSET_TYPE = 'es3'
def wrap_parsed_args(parsed_args):
"""
Function to add a method to the parsed argument object to transform a long-form argument name to and get the
parsed values based on the input long form.
This will allow us to read an argument like '--foo-bar=Orange' by using the built in method rather than looking for
the argparsed transformed attrobite 'foo_bar'
:param parsed_args: The parsed args object to wrap
"""
def parse_argument_attr(argument):
argument_attr = argument[2:].replace('-', '_')
return getattr(parsed_args, argument_attr)
parsed_args.get_argument = parse_argument_attr
def main(args):
"""
Perform the main argument processing and execution of the project generator
:param args: The arguments to process
"""
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.',
default=os.getcwd())
parser.add_argument('--build-dir',
help='The build dir subpath from the dev root',
required=True)
parser.add_argument('--third-party-path',
help='The path to the 3rd Party root directory',
required=True)
parser.add_argument(ANDROID_NDK_ARGUMENT_NAME,
help='The path to the android NDK',
required=True)
parser.add_argument(ANDROID_SDK_ARGUMENT_NAME,
help='The path to the android SDK',
required=True)
parser.add_argument(ANDROID_SDK_PLATFORM_ARGUMENT_NAME,
help='The android SDK version',
required=True)
parser.add_argument(ANDROID_SDK_PREFERRED_TOOL_VER,
help='The preferred android sdk build version (i.e. 28.0.3). Will default to the first one detected under the android sdk',
default=None,
required=False)
parser.add_argument(ANDROID_NDK_PLATFORM_ARGUMENT_NAME,
help='The android NDK version',
required=True)
parser.add_argument(GRADLE_ARGUMENT_NAME,
help=f'The path to installed gradle. The version of gradle must fall in between {str(GRADLE_MIN_VERSION)} and {str(GRADLE_MAX_VERSION)}.',
default=None,
required=False)
parser.add_argument(CMAKE_ARGUMENT_NAME,
help=f'The path to cmake build tool if not installed on the system path. The version of cmake must be at least version {str(CMAKE_MIN_VERSION)}.',
default=None,
required=False)
parser.add_argument(NINJA_ARGUMENT_NAME,
help='The path to the ninja build tool if not installed on the system path.',
default=None,
required=False)
parser.add_argument('-g', '--game-name',
help='The game project to base off of')
# Asset Options
parser.add_argument(INCLUDE_APK_ASSETS_ARGUMENT_NAME,
action='store_true',
help='Option to include the game assets when building the APK. If this option is set, you must have the android assets built.')
parser.add_argument(ASSET_MODE_ARGUMENT_NAME,
choices=ALL_ASSET_MODES,
default=ASSET_MODE_LOOSE,
help='Asset Mode (vfs|pak|loose) to use when including assets into the APK')
parser.add_argument(ASSET_TYPE_ARGUMENT_NAME,
default=DEFAULT_ASSET_TYPE,
help='Asset Type to use when including assets into the APK')
parser.add_argument('--debug',
action='store_true',
help='Enable debug logs.')
# Signing Config options
parser.add_argument(SIGNING_PROFILE_STORE_FILE_ARGUMENT_NAME,
default=None,
help='(Optional) If specified, create a signing profile based on this supplied android jks keystore file.')
parser.add_argument(SIGNING_PROFILE_STORE_PASSWORD_ARGUMENT_NAME,
default=None,
help='If an android jks keystore file is specified, this is the store password for the keystore.')
parser.add_argument(SIGNING_PROFILE_KEY_ALIAS_ARGUMENT_NAME,
default=None,
help='If an android jks keystore file is specified, this is the alias of the signing key in the keystore.')
parser.add_argument(SIGNING_PROFILE_KEY_PASSWORD_ARGUMENT_NAME,
default=None,
help='If an android jks keystore file is specified, this is the password of the signing key in the keystore.')
parser.add_argument('--unit-test',
action='store_true',
help='Generate a unit test APK instead of a game APK.')
parsed_args = parser.parse_args(args)
wrap_parsed_args(parsed_args)
# Prepare the logging
logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG if parsed_args.debug else logging.INFO)
# Verify the gradle requirements
gradle_version, override_gradle_path = verify_gradle(override_gradle_path=parsed_args.get_argument(GRADLE_ARGUMENT_NAME))
logging.info("Detected Gradle version %s", str(gradle_version))
# Verify the cmake requirements
cmake_version, override_cmake_path = verify_cmake(override_cmake_path=parsed_args.get_argument(CMAKE_ARGUMENT_NAME))
logging.info("Detected CMake version %s", str(cmake_version))
# Verify the ninja requirements
ninja_version, override_ninja_path = verify_ninja(override_ninja_path=parsed_args.get_argument(NINJA_ARGUMENT_NAME))
logging.info("Detected Ninja version %s", str(ninja_version))
# Verify the android sdk path and sdk version
verified_android_sdk_platform, verified_android_sdk_path, android_sdk_build_tool_ver = android_support.verify_android_sdk(android_sdk_platform=parsed_args.get_argument(ANDROID_SDK_PLATFORM_ARGUMENT_NAME),
argument_name=ANDROID_SDK_ARGUMENT_NAME,
override_android_sdk_path=parsed_args.get_argument(ANDROID_SDK_ARGUMENT_NAME),
preferred_sdk_build_tools_ver=parsed_args.get_argument(ANDROID_SDK_PREFERRED_TOOL_VER))
# Verify the android ndk path and ndk version
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)
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)
is_test_project = False
# Verify the 3rd Party Root Path
third_party_path = pathlib.Path(parsed_args.third_party_path) / '3rdParty.txt'
if not third_party_path.is_file():
raise common.LmbrCmdError("Invalid --third-party-path '{}'. Make sure it exists and contains "
"3rdParty.txt".format(parsed_args.third_party_path),
common.ERROR_CODE_INVALID_PARAMETER)
third_party_path = third_party_path.parent
build_dir = verified_dev_root / 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("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,
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,
override_gradle_path=override_gradle_path,
override_ninja_path=override_ninja_path,
android_sdk_build_tool_version=android_sdk_build_tool_ver,
include_assets_in_apk=parsed_args.get_argument(INCLUDE_APK_ASSETS_ARGUMENT_NAME),
asset_mode=parsed_args.get_argument(ASSET_MODE_ARGUMENT_NAME),
asset_type=parsed_args.get_argument(ASSET_TYPE_ARGUMENT_NAME),
signing_config=signing_config,
is_test_project=is_test_project)
generator.execute()
if __name__ == '__main__':
try:
main(sys.argv[1:])
exit(0)
except common.LmbrCmdError as err:
print(str(err), file=sys.stderr)
exit(err.code)
@@ -0,0 +1,295 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
import argparse
import logging
import os
import pathlib
import queue
import re
import sys
import threading
import time
# 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)
from cmake.Tools import common
from cmake.Tools.Platform.Android import android_support
# The name of the unit test target
TEST_PROJECT = 'AzTestRunner'
TEST_ACTIVITY = 'AzTestRunnerActivity'
# Prepare a regex that will strip out the timestamp and PID information from adb's logcat to just show 'LMBR' tagged logs
REGEX_LOGCAT_LINE = re.compile(r'([\d-]+)\s+([\d:\.]+)\s+(\d+)\s+(\d+)\s+(I)\s+(LMBR)([\s]+)(:\s)(.*)')
# The startup delay will allow the test runner to pause so that the we have a chance to query for the PID of the test launcher
TEST_RUNNER_STARTUP_DELAY = 1
LOGCAT_BUFFER_SIZE_MB = 32
LOGCAT_READ_QUEUE = queue.Queue()
def validate_android_test_build_dir(build_dir, configuration):
"""
Validate an android test build folder
:param build_dir: The build directory where the android test project was generated.
:param configuration: The configuration of the test build
:return: tuple of (Path of build_dir, path of the build dir native path (where the native binaries are built for the configuration), and the android AdbTool wrapper
"""
build_path = pathlib.Path(build_dir) if os.path.isabs(build_dir) else pathlib.Path(ROOT_DEV_PATH) / build_dir
if not build_path.is_dir():
raise common.LmbrCmdError(f"Invalid android build directory '{str(build_path)}'")
# Get the platform settings to validate the test game name (must match TEST_PROJECT)
platform_settings = common.PlatformSettings(build_path)
if not platform_settings.projects:
raise common.LmbrCmdError("Missing required platform settings object from build directory.")
game_name = platform_settings.projects[0]
if game_name != TEST_PROJECT:
raise common.LmbrCmdError("Invalid android build folder for tests.")
# Construct and validate the path to the native binaries that are built for the APK based on the input confiugration
build_configuration_path = build_path / 'app' / 'cmake' / configuration / 'arm64-v8a'
if not build_configuration_path.is_dir():
raise common.LmbrCmdError(f"Invalid android build configuration '{configuration}': Make sure that the APK has been built with this configuration successfully")
# Validate the android SDK path that was registered in the platform settings
android_sdk_path = getattr(platform_settings, 'android_sdk_path', None)
if not android_sdk_path:
raise common.LmbrCmdError(f"Android SDK Path {android_sdk_path} is missing in the platform settings for {build_dir}.")
if not os.path.isdir(android_sdk_path):
raise common.LmbrCmdError(f"Android SDK Path {android_sdk_path} in the platform settings for {build_dir} is not valid.")
return build_path, build_configuration_path, android_sdk_path
def launch_test_on_device(adb_tool, test_module, timeout_secs, test_filter):
"""
Launch an test module on the connect android device
:param adb_tool: The ADB Tool to exec the adb commands necessary to run a test
:param test_module: The name of the test module to run
:param timeout_secs: Timeout for a test run
:return: True if the tests passed, False if not
"""
# Clear user data before each test
adb_tool.exec(['shell', 'pm', 'clear', 'com.lumberyard.tests'])
# Increase the log buffer to prevent 'end of file' error from logcat
adb_tool.exec(['shell', 'logcat', '-G', f'{LOGCAT_BUFFER_SIZE_MB}M'])
# Start the test activity
exec_args = ['shell', 'am', 'start',
'-n', f'com.lumberyard.tests/.{TEST_ACTIVITY}',
'--es', test_module, 'AzRunUnitTests',
'--es', 'startdelay', str(TEST_RUNNER_STARTUP_DELAY)]
if test_filter:
exec_args.extend([
'--es', 'gtest_filter', test_filter
])
ret, result_output, result_error = adb_tool.exec(exec_args, capture_stdout=True)
if ret != 0:
raise common.LmbrCmdError(f"Unable to launch test runner activity: {result_error or result_output}")
tests_passed = False
result_pid = None
try:
# Make multiple attempts to get the PID of the test process
max_pid_retries = 5
while max_pid_retries > 0:
ret, result_pid, _ = adb_tool.exec(['shell', 'pidof', '-s', 'com.lumberyard.tests'], capture_stdout=True)
if ret == 0:
break
time.sleep(1)
max_pid_retries -= 1
if not result_pid:
raise common.LmbrCmdError("Unable to get process id for the Test Runner launcher")
result_pid = result_pid.strip()
# Start the adb logcat process for the result PID and filter the stdout
logcat_proc = adb_tool.popen(['shell', 'logcat', f'--pid={result_pid}', '-s', 'LMBR'])
start_time = time.time()
while logcat_proc.poll() is None:
# Break out of the loop if we triggered the test timeout condition (timeout_secs)
elapsed_time = time.time() - start_time
if elapsed_time > timeout_secs > 0:
logging.error("Test Runner Timeout")
break
# Break out of the loop in case the process dies unexpectly
ret, _, _ = adb_tool.exec(['shell', 'pidof', '-s', 'com.lumberyard.tests'], capture_stdout=True)
if ret != 0:
break
# Use a regex to strip out timestamp/PID logcat line and filter only the 'LMBR' tagged log events
line = logcat_proc.stdout.readline()
result = REGEX_LOGCAT_LINE.match(line) if line else None
if result:
lmbr_log_line = result.group(9)
print(lmbr_log_line)
if '[FAILURE]' in lmbr_log_line:
break
if '[SUCCESS]' in lmbr_log_line:
tests_passed = True
break
finally:
if result_pid:
adb_tool.exec(['shell', 'logcat', f'--pid={result_pid}', '-c'])
# Kill the test launcher process
adb_tool.exec(['shell', 'am', 'force-stop', 'com.lumberyard.tests'])
time.sleep(2)
return tests_passed
def launch_android_test(build_dir, configuration, target_dev_serial, test_target, timeout_secs, test_filter):
"""
Launch the unit test android apk with specific test target(s)
:param build_dir: The cmake build directory to base the launch values on
:param configuration: The configuration to base the launch values on
:param target_dev_serial: The target device serial number to launch the test on. If none, launch on all connected devices
:param test_target: The name of the target module to invoke the test on. If 'all' is specified, then iterate through all of the test modules and launch them individually
:param timeout_secs: Timeout value for individual test runs
:return: True if the test run(s) were successful, false if not
"""
# Validate the build dir and configuration
build_path, build_configuration_path, android_sdk_path = validate_android_test_build_dir(build_dir=build_dir,
configuration=configuration)
test_targets = common.get_validated_test_modules(test_modules=test_target, build_dir_path=build_dir)
# Track the long text length for formatting/alignment for the final report
max_module_text_len = max([len(module) for module in test_targets])
module_column_width = max_module_text_len + 8
adb_tool = android_support.AdbTool(android_sdk_path)
adb_tool.connect(target_dev_serial)
start_time = time.time()
final_report_map = {}
test_run_complete_event = threading.Event()
successful_run = True
for test_target in test_targets:
logging.info(f"Launching test for module {test_target}")
result = launch_test_on_device(adb_tool, test_target, timeout_secs, test_filter)
if result:
logging.info(f"Tests for module {test_target} Passed")
final_report_map[test_target] = 'PASSED'
else:
logging.info(f"Tests for module {test_target} Failed")
final_report_map[test_target] = 'FAILED'
successful_run = False
time.sleep(1)
end_time = time.time()
elapsed_time = end_time - start_time
hours = elapsed_time // 3600
elapsed_time = elapsed_time - 3600 * hours
minutes = elapsed_time // 60
seconds = elapsed_time - 60 * minutes
logging.info(f"Total Time : {int(hours)}h {int(minutes)}m {int(seconds)}s")
logging.info(f"Test Modules: {len(test_targets)}")
logging.info('----------------------------------------------------')
for test_module, test_result in final_report_map.items():
module_text_len = len(test_module)
logging.info(f"{test_module}{' '* (module_column_width-module_text_len)} : {test_result}")
test_run_complete_event.set()
adb_tool.disconnect()
return successful_run
def main(args):
parser = argparse.ArgumentParser(description="Launch a test module on a target dev kit.")
parser.add_argument('-b', '--build-dir',
help=f'The relative build directory to deploy from.',
required=True)
parser.add_argument('-c', '--configuration',
help='The build configuration from the build directory for the source deployment files',
default='profile')
parser.add_argument('test_module',
nargs='*',
help="The test module(s) to launch on the target device. Defaults to all registered test modules",
default=[])
parser.add_argument('--device-serial',
help='The optional device serial to target the launch on. Defaults to the all devices connected.',
default=None)
parser.add_argument('--timeout',
help='The timeout in secs for each test module to prevent deadlocked tests',
type=int,
default=-1)
parser.add_argument('--test-filter',
help="Option gtest filter to pass along to the unit test launcher",
default=None)
parser.add_argument('--debug',
help='Enable debug logging',
action='store_true')
parsed_args = parser.parse_args(args)
logging.basicConfig(format='%(levelname)s: %(message)s',
level=logging.DEBUG if parsed_args.debug else logging.INFO)
result = launch_android_test(build_dir=parsed_args.build_dir,
configuration=parsed_args.configuration,
target_dev_serial=parsed_args.device_serial,
test_target=parsed_args.test_module,
timeout_secs=int(parsed_args.timeout),
test_filter=parsed_args.test_filter)
return 0 if result else 1
if __name__ == '__main__':
try:
result_code = main(sys.argv[1:])
exit(result_code)
except common.LmbrCmdError as err:
logging.error(str(err))
exit(err.code)
@@ -0,0 +1,853 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
import datetime
import os
import pathlib
import platform
import pytest
import time
from unittest.mock import patch, Mock
from cmake.Tools.Platform.Android import android_deployment
TEST_GAME_NAME = "Foo"
TEST_DEV_ROOT = pathlib.Path("Foo")
TEST_ASSET_MODE = 'LOOSE'
TEST_ASSET_TYPE = 'es3'
TEST_ANDROID_SDK_PATH = pathlib.Path('c:\\AndroidSDK')
TEST_BUILD_DIR = 'android_gradle_test'
TEST_DEVICE_ID = '9A201FFAZ000ER'
def match_arg_list(input_args, expected_args):
if len(input_args) != len(expected_args):
return False
for index in range(len(input_args)):
if input_args[index] != expected_args[index]:
return False
return True
@patch('cmake.Tools.Platform.Android.android_deployment.AndroidDeployment.read_android_settings', return_value={})
@patch('cmake.Tools.Platform.Android.android_deployment.AndroidDeployment.resolve_adb_tool', return_value=pathlib.Path("Foo"))
def test_Initialize(mock_resolve_adb_tool, mock_read_android_settings):
attrs = {'glob.return_value': ["foo.bar"]}
mock_local_asset_path = Mock(**attrs)
inst = android_deployment.AndroidDeployment(dev_root=TEST_DEV_ROOT,
build_dir=TEST_BUILD_DIR,
configuration='profile',
game_name=TEST_GAME_NAME,
asset_mode=TEST_ASSET_MODE,
asset_type=TEST_ASSET_TYPE,
embedded_assets=True,
android_device_filter=None,
clean_deploy=False,
android_sdk_path=TEST_ANDROID_SDK_PATH,
deployment_type=android_deployment.AndroidDeployment.DEPLOY_BOTH)
mock_resolve_adb_tool.assert_called_once_with(TEST_ANDROID_SDK_PATH)
mock_read_android_settings.assert_called_once_with(TEST_DEV_ROOT, TEST_GAME_NAME)
assert inst
def test_read_android_settings(tmpdir):
game_name = "Foo"
tmpdir.ensure(f'dev_root/{game_name}/project.json')
game_project_json_file = tmpdir.join(f'dev_root/{game_name}/project.json')
game_project_json_file.write(f'{{"android_settings": {{"game_name": "{game_name.lower()}" }} }}')
result = android_deployment.AndroidDeployment.read_android_settings(pathlib.Path(tmpdir.join('dev_root').realpath()), game_name)
assert result['game_name'] == game_name.lower()
def test_resolve_adb_tool(tmpdir):
sdk_path = 'android_sdk'
adb_target = 'adb.exe' if platform.system() == 'Windows' else 'adb'
tmpdir.ensure(f'{sdk_path}/platform-tools/{adb_target}')
dummy_adb_file = tmpdir.join(f'{sdk_path}/platform-tools/{adb_target}')
dummy_adb_file.write('adb')
result = android_deployment.AndroidDeployment.resolve_adb_tool(pathlib.Path(tmpdir.join(sdk_path).realpath()))
assert pathlib.Path(dummy_adb_file.realpath()) == result
@patch('subprocess.check_output', return_value=b'PASS')
def test_adb_call(mock_check_output):
with patch.object(android_deployment.AndroidDeployment, 'read_android_settings', return_value={}), \
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',
game_name=TEST_GAME_NAME,
asset_mode=TEST_ASSET_MODE,
asset_type=TEST_ASSET_TYPE,
embedded_assets=True,
android_device_filter=None,
clean_deploy=False,
android_sdk_path=TEST_ANDROID_SDK_PATH,
deployment_type=android_deployment.AndroidDeployment.DEPLOY_BOTH)
result = inst.adb_call(arg_list=['foo'], device_id='123')
mock_check_output.assert_called_once()
assert result == 'PASS'
@patch('cmake.Tools.Platform.Android.android_deployment.AndroidDeployment.adb_call', return_value='PASS')
def test_adb_shell(mock_adb_call):
with patch.object(android_deployment.AndroidDeployment, 'read_android_settings', return_value={}), \
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',
game_name=TEST_GAME_NAME,
asset_mode=TEST_ASSET_MODE,
asset_type=TEST_ASSET_TYPE,
embedded_assets=True,
android_device_filter=None,
clean_deploy=False,
android_sdk_path=TEST_ANDROID_SDK_PATH,
deployment_type=android_deployment.AndroidDeployment.DEPLOY_BOTH)
result = inst.adb_shell(command='foo me', device_id='123')
expected_args = ['shell', 'foo me']
mock_adb_call.assert_called_once_with(expected_args, device_id='123')
assert result == 'PASS'
@patch('cmake.Tools.Platform.Android.android_deployment.AndroidDeployment.adb_shell', return_value='output')
def test_adb_ls_success(mock_adb_shell):
with patch.object(android_deployment.AndroidDeployment, 'read_android_settings', return_value={}), \
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',
game_name=TEST_GAME_NAME,
asset_mode=TEST_ASSET_MODE,
asset_type=TEST_ASSET_TYPE,
embedded_assets=True,
android_device_filter=None,
clean_deploy=False,
android_sdk_path=TEST_ANDROID_SDK_PATH,
deployment_type=android_deployment.AndroidDeployment.DEPLOY_BOTH)
result, output = inst.adb_ls(path='/foo/bar', device_id='123')
mock_adb_shell.assert_called_once_with(command='ls /foo/bar', device_id='123')
assert result
assert output == 'output'
@patch('cmake.Tools.Platform.Android.android_deployment.AndroidDeployment.adb_shell', return_value='')
def test_adb_ls_error_no_output(mock_adb_shell):
with patch.object(android_deployment.AndroidDeployment, 'read_android_settings', return_value={}), \
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',
game_name=TEST_GAME_NAME,
asset_mode=TEST_ASSET_MODE,
asset_type=TEST_ASSET_TYPE,
embedded_assets=True,
android_device_filter=None,
clean_deploy=False,
android_sdk_path=TEST_ANDROID_SDK_PATH,
deployment_type=android_deployment.AndroidDeployment.DEPLOY_BOTH)
result, output = inst.adb_ls(path='/foo/bar', device_id='123')
mock_adb_shell.assert_called_once_with(command='ls /foo/bar', device_id='123')
assert not result
assert output is None
@patch('cmake.Tools.Platform.Android.android_deployment.AndroidDeployment.adb_shell', return_value='No such file or directory')
def test_adb_ls_error_no_such_file(mock_adb_shell):
with patch.object(android_deployment.AndroidDeployment, 'read_android_settings', return_value={}), \
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',
game_name=TEST_GAME_NAME,
asset_mode=TEST_ASSET_MODE,
asset_type=TEST_ASSET_TYPE,
embedded_assets=True,
android_device_filter=None,
clean_deploy=False,
android_sdk_path=TEST_ANDROID_SDK_PATH,
deployment_type=android_deployment.AndroidDeployment.DEPLOY_BOTH)
result, output = inst.adb_ls(path='/foo/bar', device_id='123')
mock_adb_shell.assert_called_once_with(command='ls /foo/bar', device_id='123')
assert not result
assert output == 'No such file or directory'
@patch('cmake.Tools.Platform.Android.android_deployment.AndroidDeployment.adb_shell', return_value='Permission denied')
def test_adb_ls_error_permission_denied(mock_adb_shell):
with patch.object(android_deployment.AndroidDeployment, 'read_android_settings', return_value={}), \
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',
game_name=TEST_GAME_NAME,
asset_mode=TEST_ASSET_MODE,
asset_type=TEST_ASSET_TYPE,
embedded_assets=True,
android_device_filter=None,
clean_deploy=False,
deployment_type=android_deployment.AndroidDeployment.DEPLOY_BOTH,
android_sdk_path=TEST_ANDROID_SDK_PATH)
result, output = inst.adb_ls(path='/foo/bar', device_id='123')
mock_adb_shell.assert_called_once_with(command='ls /foo/bar', device_id='123')
assert not result
assert output == 'Permission denied'
@patch('cmake.Tools.Platform.Android.android_deployment.AndroidDeployment.adb_call',
return_value=f'List of devices attached{os.linesep}9A201FFAZ000ER device{os.linesep}1A201FFAZ000ER device{os.linesep}9A201FFAZ456ER unauthorized')
def test_get_target_android_devices(mock_adb_call):
with patch.object(android_deployment.AndroidDeployment, 'read_android_settings', return_value={}), \
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',
game_name=TEST_GAME_NAME,
asset_mode=TEST_ASSET_MODE,
asset_type=TEST_ASSET_TYPE,
embedded_assets=True,
android_device_filter=f'{TEST_DEVICE_ID},AAAAASDSFGG',
clean_deploy=False,
deployment_type=android_deployment.AndroidDeployment.DEPLOY_BOTH,
android_sdk_path=TEST_ANDROID_SDK_PATH)
result = inst.get_target_android_devices()
mock_adb_call.assert_called_once_with("devices")
assert len(result) == 1
assert result[0] == TEST_DEVICE_ID
@patch('cmake.Tools.Platform.Android.android_deployment.AndroidDeployment.adb_ls', return_value=(True,"file"))
def test_check_known_android_paths_success(mock_adb_ls):
with patch.object(android_deployment.AndroidDeployment, 'read_android_settings', return_value={}), \
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',
game_name=TEST_GAME_NAME,
asset_mode=TEST_ASSET_MODE,
asset_type=TEST_ASSET_TYPE,
embedded_assets=True,
android_device_filter=None,
clean_deploy=False,
deployment_type=android_deployment.AndroidDeployment.DEPLOY_BOTH,
android_sdk_path=TEST_ANDROID_SDK_PATH)
result = inst.check_known_android_paths(device_id='123')
mock_adb_ls.assert_called_once()
assert result == android_deployment.KNOWN_ANDROID_EXTERNAL_STORAGE_PATHS[0][:-1]
@patch('cmake.Tools.Platform.Android.android_deployment.AndroidDeployment.adb_ls', return_value=(False,None))
def test_check_known_android_paths_fail(mock_adb_ls):
with patch.object(android_deployment.AndroidDeployment, 'read_android_settings', return_value={}), \
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',
game_name=TEST_GAME_NAME,
asset_mode=TEST_ASSET_MODE,
asset_type=TEST_ASSET_TYPE,
embedded_assets=True,
android_device_filter=None,
clean_deploy=False,
deployment_type=android_deployment.AndroidDeployment.DEPLOY_BOTH,
android_sdk_path=TEST_ANDROID_SDK_PATH)
result = inst.check_known_android_paths(device_id='123')
assert not result
assert mock_adb_ls.call_count == len(android_deployment.KNOWN_ANDROID_EXTERNAL_STORAGE_PATHS)
@patch('cmake.Tools.Platform.Android.android_deployment.AndroidDeployment.adb_shell', return_value=None)
@patch('cmake.Tools.Platform.Android.android_deployment.AndroidDeployment.check_known_android_paths', return_value="PATH")
def test_detect_device_storage_path_no_external_storage_env(mock_check_known_android_paths, mock_adb_shell):
with patch.object(android_deployment.AndroidDeployment, 'read_android_settings', return_value={}), \
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',
game_name=TEST_GAME_NAME,
asset_mode=TEST_ASSET_MODE,
asset_type=TEST_ASSET_TYPE,
embedded_assets=True,
android_device_filter=None,
clean_deploy=False,
deployment_type=android_deployment.AndroidDeployment.DEPLOY_BOTH,
android_sdk_path=TEST_ANDROID_SDK_PATH)
result = inst.detect_device_storage_path(device_id=TEST_DEVICE_ID)
assert result == "PATH"
mock_adb_shell.assert_called_once()
mock_check_known_android_paths.assert_called_once_with(TEST_DEVICE_ID)
@patch('cmake.Tools.Platform.Android.android_deployment.AndroidDeployment.adb_shell', return_value="NotSet")
@patch('cmake.Tools.Platform.Android.android_deployment.AndroidDeployment.check_known_android_paths', return_value="PATH")
def test_detect_device_storage_path_invalid_external_storage_env(mock_check_known_android_paths, mock_adb_shell):
with patch.object(android_deployment.AndroidDeployment, 'read_android_settings', return_value={}), \
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',
game_name=TEST_GAME_NAME,
asset_mode=TEST_ASSET_MODE,
asset_type=TEST_ASSET_TYPE,
embedded_assets=True,
android_device_filter=None,
clean_deploy=False,
deployment_type=android_deployment.AndroidDeployment.DEPLOY_BOTH,
android_sdk_path=TEST_ANDROID_SDK_PATH)
result = inst.detect_device_storage_path(device_id=TEST_DEVICE_ID)
assert result == "PATH"
mock_adb_shell.assert_called_once()
mock_check_known_android_paths.assert_called_once_with(TEST_DEVICE_ID)
@patch('cmake.Tools.Platform.Android.android_deployment.AndroidDeployment.adb_shell', return_value="EXTERNAL_STORAGE=/foo/bar")
@patch('cmake.Tools.Platform.Android.android_deployment.AndroidDeployment.adb_ls', return_value=(True, "foo.bar"))
def test_detect_device_storage_path_valid_external_storage_env(mock_adb_ls, mock_adb_shell):
with patch.object(android_deployment.AndroidDeployment, 'read_android_settings', return_value={}), \
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',
game_name=TEST_GAME_NAME,
asset_mode=TEST_ASSET_MODE,
asset_type=TEST_ASSET_TYPE,
embedded_assets=True,
android_device_filter=None,
clean_deploy=False,
deployment_type=android_deployment.AndroidDeployment.DEPLOY_BOTH,
android_sdk_path=TEST_ANDROID_SDK_PATH)
result = inst.detect_device_storage_path(device_id=TEST_DEVICE_ID)
assert result == "/foo/bar"
mock_adb_shell.assert_called_once()
mock_adb_ls.assert_called_once_with(path='/foo/bar', device_id=TEST_DEVICE_ID)
def test_detect_device_storage_path_real_path():
def _mock_adb_shell(command, device_id):
if command == "set | grep EXTERNAL_STORAGE":
return "EXTERNAL_STORAGE=/foo/bar"
elif command == f'realpath /foo/bar':
return "/foo_reals"
else:
raise AssertionError
def _mock_adb_ls(path, device_id, args=None):
if path == "/foo/bar":
return False, None
elif path == '/foo_reals':
return True, "foo.bar"
else:
raise AssertionError
with patch.object(android_deployment.AndroidDeployment, 'read_android_settings', return_value={}), \
patch.object(android_deployment.AndroidDeployment, 'resolve_adb_tool', return_value=pathlib.Path("Foo")), \
patch.object(android_deployment.AndroidDeployment, 'adb_shell', wraps=_mock_adb_shell), \
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',
game_name=TEST_GAME_NAME,
asset_mode=TEST_ASSET_MODE,
asset_type=TEST_ASSET_TYPE,
embedded_assets=True,
android_device_filter=None,
clean_deploy=False,
deployment_type=android_deployment.AndroidDeployment.DEPLOY_BOTH,
android_sdk_path=TEST_ANDROID_SDK_PATH)
result = inst.detect_device_storage_path(device_id=TEST_DEVICE_ID)
assert result == "/foo_reals"
@patch('cmake.Tools.Platform.Android.android_deployment.AndroidDeployment.check_known_android_paths', return_value="PATH")
def test_detect_device_storage_path_real_path_fail(mock_check_known_android_paths):
def _mock_adb_shell(command, device_id):
if command == "set | grep EXTERNAL_STORAGE":
return "EXTERNAL_STORAGE=/foo/bar"
elif command == f'realpath /foo/bar':
return "/foo_reals"
else:
raise AssertionError
def _mock_adb_ls(path, device_id, args=None):
if path == "/foo/bar":
return False, None
elif path == '/foo_reals':
return False, None
else:
raise AssertionError
with patch.object(android_deployment.AndroidDeployment, 'read_android_settings', return_value={}), \
patch.object(android_deployment.AndroidDeployment, 'resolve_adb_tool', return_value=pathlib.Path("Foo")), \
patch.object(android_deployment.AndroidDeployment, 'adb_shell', wraps=_mock_adb_shell), \
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',
game_name=TEST_GAME_NAME,
asset_mode=TEST_ASSET_MODE,
asset_type=TEST_ASSET_TYPE,
embedded_assets=True,
android_device_filter=None,
clean_deploy=False,
deployment_type=android_deployment.AndroidDeployment.DEPLOY_BOTH,
android_sdk_path=TEST_ANDROID_SDK_PATH)
result = inst.detect_device_storage_path(device_id=TEST_DEVICE_ID)
assert result == "PATH"
mock_check_known_android_paths.assert_called_once_with(TEST_DEVICE_ID)
@patch('cmake.Tools.Platform.Android.android_deployment.AndroidDeployment.adb_shell', return_value="2020-04-30 09:20:00.0000")
def test_get_device_file_timestamp_success(mock_adb_shell):
with patch.object(android_deployment.AndroidDeployment, 'read_android_settings', return_value={}), \
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',
game_name=TEST_GAME_NAME,
asset_mode=TEST_ASSET_MODE,
asset_type=TEST_ASSET_TYPE,
embedded_assets=True,
android_device_filter=None,
clean_deploy=False,
deployment_type=android_deployment.AndroidDeployment.DEPLOY_BOTH,
android_sdk_path=TEST_ANDROID_SDK_PATH)
remote_path = "/foo/bar/timestamp.txt"
result = inst.get_device_file_timestamp(remote_file_path=remote_path,
device_id=TEST_DEVICE_ID)
assert result == time.mktime(time.strptime("2020-04-30 09:20:00.0000", '%Y-%m-%d %H:%M:%S.%f'))
mock_adb_shell.assert_called_once_with(command=f'cat {remote_path}',
device_id=TEST_DEVICE_ID)
@patch('cmake.Tools.Platform.Android.android_deployment.AndroidDeployment.adb_shell', return_value=None)
def test_get_device_file_timestamp_no_file(mock_adb_shell):
with patch.object(android_deployment.AndroidDeployment, 'read_android_settings', return_value={}), \
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',
game_name=TEST_GAME_NAME,
asset_mode=TEST_ASSET_MODE,
asset_type=TEST_ASSET_TYPE,
embedded_assets=True,
android_device_filter=None,
clean_deploy=False,
deployment_type=android_deployment.AndroidDeployment.DEPLOY_BOTH,
android_sdk_path=TEST_ANDROID_SDK_PATH)
remote_path = "/foo/bar/timestamp.txt"
result = inst.get_device_file_timestamp(remote_file_path=remote_path,
device_id=TEST_DEVICE_ID)
assert not result
mock_adb_shell.assert_called_once_with(command=f'cat {remote_path}',
device_id=TEST_DEVICE_ID)
@patch('cmake.Tools.Platform.Android.android_deployment.AndroidDeployment.adb_shell', return_value="ZZZZXXXX")
def test_get_device_file_timestamp_bad_timestamp_file(mock_adb_shell):
with patch.object(android_deployment.AndroidDeployment, 'read_android_settings', return_value={}), \
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',
game_name=TEST_GAME_NAME,
asset_mode=TEST_ASSET_MODE,
asset_type=TEST_ASSET_TYPE,
embedded_assets=True,
android_device_filter=None,
clean_deploy=False,
deployment_type=android_deployment.AndroidDeployment.DEPLOY_BOTH,
android_sdk_path=TEST_ANDROID_SDK_PATH)
remote_path = "/foo/bar/timestamp.txt"
result = inst.get_device_file_timestamp(remote_file_path=remote_path,
device_id=TEST_DEVICE_ID)
assert not result
mock_adb_shell.assert_called_once_with(command=f'cat {remote_path}',
device_id=TEST_DEVICE_ID)
def test_update_device_file_timestamp(tmpdir):
cache_dir = f'{TEST_DEV_ROOT}/Cache/{TEST_GAME_NAME}/{TEST_ASSET_TYPE}'
tmpdir.ensure(f'{cache_dir}/foo.txt')
mock_dev_root = tmpdir.join(TEST_DEV_ROOT).realpath()
with patch.object(android_deployment.AndroidDeployment, 'read_android_settings', return_value={}), \
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',
game_name=TEST_GAME_NAME,
asset_mode=TEST_ASSET_MODE,
asset_type=TEST_ASSET_TYPE,
embedded_assets=True,
android_device_filter=None,
clean_deploy=False,
deployment_type=android_deployment.AndroidDeployment.DEPLOY_ASSETS_ONLY,
android_sdk_path=TEST_ANDROID_SDK_PATH)
remote_path = "/foo/bar/timestamp.txt"
inst.update_device_file_timestamp(relative_assets_path=remote_path,
device_id=TEST_DEVICE_ID)
mock_adb_call.assert_called()
@pytest.mark.parametrize(
"test_config, test_package_name, test_device_storage_path", [
pytest.param('profile', 'com.amazon.lumberyard.foo', '/data/fool_storage'),
pytest.param('debug', 'com.amazon.lumberyard.foo', '/data/fool_storage'),
pytest.param('profile', 'com.amazon.lumberyard.bar', '/data/fool_storage'),
pytest.param('debug', 'com.amazon.lumberyard.bar', '/data/fool_storage'),
pytest.param('profile', 'com.amazon.lumberyard.foo', '/data/fool_storage2'),
pytest.param('debug', 'com.amazon.lumberyard.foo', '/data/fool_storage2'),
pytest.param('profile', 'com.amazon.lumberyard.bar', '/data/fool_storage2'),
pytest.param('debug', 'com.amazon.lumberyard.bar', '/data/fool_storage2')
]
)
def test_execute_success(tmpdir, test_config, test_package_name, test_device_storage_path):
mock_dev_root = tmpdir.join(TEST_DEV_ROOT).realpath()
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_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())
expected_storage_registry_path = f'{test_device_storage_path}/Android/data/{test_package_name}/files/Registry'
def _mock_adb_call(arg_list, device_id=None):
if arg_list == "start-server":
return "SUCCESS"
if arg_list == "kill-server":
return "SUCCESS"
elif isinstance(arg_list, list):
if match_arg_list(arg_list, ['install', '-r', expected_apk_path]):
return "SUCCESS"
elif match_arg_list(arg_list, ['install', '-t', '-r', expected_apk_path]):
return "SUCCESS"
elif match_arg_list(arg_list, ['push', expected_asset_path, f'{test_device_storage_path}/Android/data/{test_package_name}/files']):
return "SUCCESS"
elif match_arg_list(arg_list, ['push', expected_registry_path, expected_storage_registry_path]):
return "SUCCESS"
raise AssertionError
with patch.object(android_deployment.AndroidDeployment, 'get_target_android_devices', return_value=[TEST_DEVICE_ID]), \
patch.object(android_deployment.AndroidDeployment, 'detect_device_storage_path', return_value=test_device_storage_path), \
patch.object(android_deployment.AndroidDeployment, 'get_device_file_timestamp', return_value=None), \
patch.object(android_deployment.AndroidDeployment, 'update_device_file_timestamp') as mock_update_device_file_timestamp, \
patch.object(android_deployment.AndroidDeployment, 'read_android_settings', return_value={'package_name': test_package_name}), \
patch.object(android_deployment.AndroidDeployment, 'resolve_adb_tool', return_value=pathlib.Path("Foo")), \
patch.object(android_deployment.AndroidDeployment, 'adb_call', wraps=_mock_adb_call) as mock_adb_call, \
patch.object(pathlib.Path, 'glob', return_value=["foo.bar"]):
inst = android_deployment.AndroidDeployment(dev_root=mock_dev_root,
build_dir=TEST_BUILD_DIR,
configuration=test_config,
game_name=TEST_GAME_NAME,
asset_mode=TEST_ASSET_MODE,
asset_type=TEST_ASSET_TYPE,
embedded_assets=False,
android_device_filter=None,
clean_deploy=False,
deployment_type=android_deployment.AndroidDeployment.DEPLOY_BOTH,
android_sdk_path=TEST_ANDROID_SDK_PATH)
inst.execute()
mock_update_device_file_timestamp.assert_called_once()
assert mock_adb_call.call_count == 5
@pytest.mark.parametrize(
"test_game_name, test_config, test_package_name, test_device_storage_path, test_asset_type", [
pytest.param('game1','profile', 'com.amazon.lumberyard.foo', '/data/fool_storage', 'es3'),
pytest.param('game1','debug', 'com.amazon.lumberyard.foo', '/data/fool_storage', 'es3'),
pytest.param('game2','profile', 'com.amazon.lumberyard.bar', '/data/fool_storage', 'es3'),
pytest.param('game2','debug', 'com.amazon.lumberyard.bar', '/data/fool_storage', 'es3'),
pytest.param('game3','profile', 'com.amazon.lumberyard.foo', '/data/fool_storage2', 'pc'),
pytest.param('game3','debug', 'com.amazon.lumberyard.foo', '/data/fool_storage2', 'pc'),
pytest.param('game4','profile', 'com.amazon.lumberyard.bar', '/data/fool_storage2', 'pc'),
pytest.param('game4','debug', 'com.amazon.lumberyard.bar', '/data/fool_storage2', 'pc')
]
)
def test_execute_clean_deploy_success(tmpdir, test_game_name, test_config, test_package_name, test_device_storage_path, test_asset_type):
mock_dev_root = tmpdir.join(TEST_DEV_ROOT).realpath()
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_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())
expected_storage_path = f'{test_device_storage_path}/Android/data/{test_package_name}/files'
expected_storage_registry_path = f'{test_device_storage_path}/Android/data/{test_package_name}/files/Registry'
def _mock_adb_call(arg_list, device_id=None):
if arg_list == "start-server":
return "SUCCESS"
if arg_list == "kill-server":
return "SUCCESS"
elif isinstance(arg_list,list):
if match_arg_list(arg_list, ['install', '-r', expected_apk_path]):
return "SUCCESS"
elif match_arg_list(arg_list, ['install', '-t', '-r', expected_apk_path]):
return "SUCCESS"
elif match_arg_list(arg_list, ['push', expected_asset_path, '-r', expected_apk_path, expected_storage_path]):
return "SUCCESS"
elif match_arg_list(arg_list, ['shell', 'cmd', 'package', 'list', 'packages', test_package_name]):
return test_package_name
elif match_arg_list(arg_list, ['uninstall', test_package_name]):
return "SUCCESS"
elif match_arg_list(arg_list, ['push', expected_asset_path, expected_storage_path]):
return "SUCCESS"
elif match_arg_list(arg_list, ['push', expected_registry_path, expected_storage_registry_path]):
return "SUCCESS"
raise AssertionError
def _mock_adb_shell(command, device_id):
assert command.startswith('rm -rf')
def _mock_adb_ls(path, device_id, args=None):
if path == "/foo/bar":
return False, None
elif path == '/foo_reals':
return False, None
else:
raise AssertionError
with patch.object(android_deployment.AndroidDeployment, 'get_target_android_devices', return_value=[TEST_DEVICE_ID]) as mock_get_target_android_devices, \
patch.object(android_deployment.AndroidDeployment, 'detect_device_storage_path', return_value=test_device_storage_path) as mock_detect_device_storage_path, \
patch.object(android_deployment.AndroidDeployment, 'get_device_file_timestamp', return_value=None) as mock_get_device_file_timestamp, \
patch.object(android_deployment.AndroidDeployment, 'update_device_file_timestamp') as mock_update_device_file_timestamp, \
patch.object(android_deployment.AndroidDeployment, 'read_android_settings', return_value={'package_name': test_package_name}), \
patch.object(android_deployment.AndroidDeployment, 'resolve_adb_tool', return_value=pathlib.Path("Foo")), \
patch.object(android_deployment.AndroidDeployment, 'adb_call', wraps=_mock_adb_call) as mock_adb_call, \
patch.object(android_deployment.AndroidDeployment, 'adb_shell', wraps=_mock_adb_shell) as mock_adb_shell, \
patch.object(android_deployment.AndroidDeployment, 'adb_ls', wraps=_mock_adb_ls), \
patch.object(pathlib.Path, 'glob', return_value=["foo.bar"]):
inst = android_deployment.AndroidDeployment(dev_root=mock_dev_root,
build_dir=TEST_BUILD_DIR,
configuration=test_config,
game_name=test_game_name,
asset_mode=TEST_ASSET_MODE,
asset_type=test_asset_type,
embedded_assets=False,
android_device_filter=None,
clean_deploy=True,
deployment_type=android_deployment.AndroidDeployment.DEPLOY_BOTH,
android_sdk_path=TEST_ANDROID_SDK_PATH)
inst.execute()
assert mock_adb_call.call_count == 7
mock_adb_shell.assert_called_once()
mock_update_device_file_timestamp.assert_called_once()
mock_get_device_file_timestamp.assert_called_once()
mock_detect_device_storage_path.assert_called_once()
mock_get_target_android_devices.assert_called_once()
@pytest.mark.parametrize(
"test_config, test_package_name, test_device_storage_path", [
pytest.param('profile', 'com.amazon.lumberyard.foo', '/data/fool_storage'),
pytest.param('debug', 'com.amazon.lumberyard.foo', '/data/fool_storage'),
pytest.param('profile', 'com.amazon.lumberyard.bar', '/data/fool_storage'),
pytest.param('debug', 'com.amazon.lumberyard.bar', '/data/fool_storage'),
pytest.param('profile', 'com.amazon.lumberyard.foo', '/data/fool_storage2'),
pytest.param('debug', 'com.amazon.lumberyard.foo', '/data/fool_storage2'),
pytest.param('profile', 'com.amazon.lumberyard.bar', '/data/fool_storage2'),
pytest.param('debug', 'com.amazon.lumberyard.bar', '/data/fool_storage2')
]
)
def test_execute_incremental_deploy_success(tmpdir, test_config, test_package_name, test_device_storage_path):
mock_dev_root = tmpdir.join(TEST_DEV_ROOT).realpath()
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_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())
expected_storage_registry_path = f'{test_device_storage_path}/Android/data/{test_package_name}/files/Registry'
def _mock_adb_call(arg_list, device_id=None):
if arg_list == "start-server":
return "SUCCESS"
if arg_list == "kill-server":
return "SUCCESS"
elif isinstance(arg_list,list):
if match_arg_list(arg_list, ['install', '-r', expected_apk_path]):
return "SUCCESS"
elif match_arg_list(arg_list, ['install', '-t', '-r', expected_apk_path]):
return "SUCCESS"
elif match_arg_list(arg_list, ['push', expected_registry_path, expected_storage_registry_path]):
return "SUCCESS"
elif len(arg_list) == 3 and arg_list[0] == 'push' and arg_list[1] == 'foo.bar':
return "SUCCESS"
raise AssertionError
def _mock_should_copy_file(check_path, check_time):
return check_path == 'foo.bar'
with patch.object(android_deployment.AndroidDeployment, 'get_target_android_devices', return_value=[TEST_DEVICE_ID]) as mock_get_target_android_devices, \
patch.object(android_deployment.AndroidDeployment, 'detect_device_storage_path', return_value=test_device_storage_path) as mock_detect_device_storage_path, \
patch.object(android_deployment.AndroidDeployment, 'get_device_file_timestamp', return_value=datetime.datetime.now()) as mock_get_device_file_timestamp, \
patch.object(android_deployment.AndroidDeployment, 'update_device_file_timestamp') as mock_update_device_file_timestamp, \
patch.object(android_deployment.AndroidDeployment, 'read_android_settings', return_value={'package_name': test_package_name}), \
patch.object(android_deployment.AndroidDeployment, 'resolve_adb_tool', return_value=pathlib.Path("Foo")), \
patch.object(android_deployment.AndroidDeployment, 'adb_call', wraps=_mock_adb_call) as mock_adb_call, \
patch.object(android_deployment.AndroidDeployment, 'should_copy_file', wraps=_mock_should_copy_file), \
patch.object(pathlib.Path, 'glob', return_value=["foo.bar", "no.bar"]):
inst = android_deployment.AndroidDeployment(dev_root=mock_dev_root,
build_dir=TEST_BUILD_DIR,
configuration=test_config,
game_name=TEST_GAME_NAME,
asset_mode=TEST_ASSET_MODE,
asset_type=TEST_ASSET_TYPE,
embedded_assets=False,
android_device_filter=None,
clean_deploy=False,
deployment_type=android_deployment.AndroidDeployment.DEPLOY_BOTH,
android_sdk_path=TEST_ANDROID_SDK_PATH)
inst.execute()
assert mock_adb_call.call_count == 5
mock_update_device_file_timestamp.assert_called_once()
mock_get_device_file_timestamp.assert_called_once()
mock_detect_device_storage_path.assert_called_once()
mock_get_target_android_devices.assert_called_once()
@@ -0,0 +1,286 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
import json
import os
import pytest
import platform
import subprocess
import sys
from distutils.version import LooseVersion
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)
from cmake.Tools import common
from cmake.Tools.Platform.Android import android_support, generate_android_project
@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 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')),
pytest.param(True, b"Gradle 5.6.4", LooseVersion('5.6.4')),
pytest.param(True, b"Gradle 1.0", common.LmbrCmdError('error', common.ERROR_CODE_ENVIRONMENT_ERROR)),
pytest.param(True, b"Gradle 26.3", common.LmbrCmdError('error', common.ERROR_CODE_ENVIRONMENT_ERROR))
]
)
def test_verify_gradle(tmpdir, from_override, version_str, expected_result):
orig_check_output = subprocess.check_output
if from_override:
gradle_script = 'gradle.bat' if platform.system() == 'Windows' else 'gradle'
tmpdir.ensure(f'gradle/bin/{gradle_script}')
override_gradle_install_path = str(tmpdir.join('gradle').realpath())
else:
override_gradle_install_path = None
def _mock_check_output(args, shell):
assert args
assert shell is True
if from_override:
assert args[0] == os.path.normpath(f'{override_gradle_install_path}/bin/{gradle_script}')
assert args[1] == '-v'
return version_str
subprocess.check_output = _mock_check_output
try:
result_version, result_override_path = generate_android_project.verify_gradle(override_gradle_install_path)
assert isinstance(expected_result, LooseVersion)
assert result_version == expected_result
if from_override:
assert os.path.normpath(result_override_path) == os.path.normpath(os.path.join(override_gradle_install_path, 'bin', gradle_script))
else:
assert result_override_path is None
except common.LmbrCmdError:
assert isinstance(expected_result, common.LmbrCmdError)
except Exception as e:
pass
finally:
subprocess.check_output = orig_check_output
@pytest.mark.parametrize(
"from_override, version_str, expected_result", [
pytest.param(False, b"cmake version 3.17.0\nKit Ware", LooseVersion('3.17.0'), id='equalMinVersion'),
pytest.param(False, b"cmake version 4.0.0\nKit Ware", LooseVersion('4.0.0'), id='greaterThanMinVersion'),
pytest.param(False, b"cmake version 1.0.0\nKit Ware", common.LmbrCmdError('error', common.ERROR_CODE_ENVIRONMENT_ERROR), id='lessThanMinVersion'),
pytest.param(True, b"cmake version 3.17.0\nKit Ware", LooseVersion('3.17.0'), id='override_equalMinVersion'),
pytest.param(True, b"cmake version 4.0.0\nKit Ware", LooseVersion('4.0.0'), id='override_greaterThanMinVersion'),
pytest.param(True, b"cmake version 1.0.0\nKit Ware", common.LmbrCmdError('error', common.ERROR_CODE_ENVIRONMENT_ERROR), id='override_lessThanMinVersion'),
]
)
def test_verify_cmake(tmpdir, from_override, version_str, expected_result):
orig_check_output = subprocess.check_output
if from_override:
cmake_exe = 'cmake.exe' if platform.system() == 'Windows' else 'cmake'
tmpdir.ensure(f'cmake/bin/{cmake_exe}')
override_cmake_install_path = str(tmpdir.join('cmake').realpath())
else:
override_cmake_install_path = None
def _mock_check_output(args, shell):
assert args
assert shell is True
if from_override:
assert args[0] == os.path.normpath(f'{override_cmake_install_path}/bin/{cmake_exe}')
assert args[1] == '--version'
return version_str
subprocess.check_output = _mock_check_output
try:
result_version, result_override_path = generate_android_project.verify_cmake(override_cmake_install_path)
assert isinstance(expected_result, LooseVersion)
assert result_version == expected_result
if from_override:
assert os.path.normpath(result_override_path) == os.path.normpath(os.path.join(override_cmake_install_path, 'bin', cmake_exe))
else:
assert result_override_path is None
except common.LmbrCmdError:
assert isinstance(expected_result, common.LmbrCmdError)
finally:
subprocess.check_output = orig_check_output
@pytest.mark.parametrize(
"from_override, version_str, expected_result", [
pytest.param(False, b"1.0.0", LooseVersion('1.0.0')),
pytest.param(False, b"1.10.0", LooseVersion('1.10.0')),
pytest.param(True, b"1.0.0", LooseVersion('1.0.0')),
pytest.param(True, b"1.10.0", LooseVersion('1.10.0'))
]
)
def test_verify_ninja(tmpdir, from_override, version_str, expected_result):
orig_check_output = subprocess.check_output
if from_override:
ninja_exe = 'ninja.exe' if platform.system() == 'Windows' else 'ninja'
tmpdir.ensure(f'ninja/{ninja_exe}')
override_cmake_install_path = str(tmpdir.join('ninja').realpath())
else:
override_cmake_install_path = None
def _mock_check_output(args, shell):
assert args
assert shell is True
if from_override:
assert args[0] == os.path.normpath(f'{override_cmake_install_path}/{ninja_exe}')
assert args[1] == '--version'
return version_str
subprocess.check_output = _mock_check_output
try:
result_version, result_override_path = generate_android_project.verify_ninja(override_cmake_install_path)
assert isinstance(expected_result, LooseVersion)
assert result_version == expected_result
if from_override:
assert os.path.normpath(result_override_path) == os.path.normpath(os.path.join(override_cmake_install_path, ninja_exe))
else:
assert result_override_path is None
except common.LmbrCmdError:
assert isinstance(expected_result, common.LmbrCmdError)
finally:
subprocess.check_output = orig_check_output
TEST_VALIDATE_VERSION_MIN = 19
TEST_VALIDATE_VERSION_MAX = 21
@pytest.mark.parametrize(
"test_input, expected", [
pytest.param('20', 20),
pytest.param('android-20', 20),
pytest.param('bad-21', "android-'XX'"),
pytest.param('10', "minimum"),
pytest.param('30', "maximum")
]
)
def test_validate_android_platform_input(test_input, expected):
try:
result = android_support.validate_android_platform_input(input_android_platform=test_input,
platform_variable_type='test',
min_version=TEST_VALIDATE_VERSION_MIN,
max_version=TEST_VALIDATE_VERSION_MAX)
assert isinstance(expected, int)
assert result == expected
except Exception as e:
assert expected in str(e)
def test_verify_android_sdk_success(tmpdir):
test_android_path = 'android_sdk'
sdk_version_number = 28
sdk_version = f'android-{sdk_version_number}'
tmpdir.ensure(f'{test_android_path}/platforms/{sdk_version}/package.xml')
tmpdir.ensure(f'{test_android_path}/build-tools/28.0.3/package.xml')
tmpdir.ensure(f'{test_android_path}/build-tools/29.0.3/package.xml')
input_sdk_path = tmpdir.join(test_android_path).realpath()
argument_name = '--android-sdk'
requested_build_tool_version = '29.0.3'
result_sdk_version, result_sdk_path, result_build_tool_version = android_support.verify_android_sdk(android_sdk_platform=sdk_version,
argument_name=argument_name,
override_android_sdk_path=input_sdk_path,
preferred_sdk_build_tools_ver=requested_build_tool_version)
assert result_sdk_version == sdk_version_number
assert result_sdk_path == input_sdk_path
assert result_build_tool_version == requested_build_tool_version
sdk_version_number_only = str(sdk_version_number)
result_sdk_version, result_sdk_path, result_build_tool_version = android_support.verify_android_sdk(android_sdk_platform=sdk_version_number_only,
argument_name=argument_name,
override_android_sdk_path=input_sdk_path)
assert result_sdk_version == sdk_version_number
assert result_sdk_path == input_sdk_path
assert result_build_tool_version == '28.0.3'
requested_build_tool_version = '30.0.3'
result_sdk_version, result_sdk_path, result_build_tool_version = android_support.verify_android_sdk(android_sdk_platform=sdk_version,
argument_name=argument_name,
override_android_sdk_path=input_sdk_path,
preferred_sdk_build_tools_ver=requested_build_tool_version)
assert result_sdk_version == sdk_version_number
assert result_sdk_path == input_sdk_path
assert result_build_tool_version == '28.0.3'
@pytest.mark.parametrize(
"desired_ndk_version_number, available_ndk_revisions, pkg_revision, mappings, expect_error", [
pytest.param(21, [21, 22, 24], '15.2.4203891', None, False, id='preNdk19ExactMatch'),
pytest.param(23, [21, 22, 24], '15.2.4203891', None, False, id='preNdk19FallbackMatch'),
pytest.param(22, [21, 22, 24], '19.2.4203891', {'23': 21}, False, id='postNdk19ExactMatch'),
pytest.param(23, [21, 22, 24], '21.2.4203891', {'23': 21}, False, id='postNdk19MappingMatch'),
pytest.param(android_support.ANDROID_NDK_MIN_PLATFORM-1, [21, 22, 24], '15.2.4203891', None, True, id='preNdk19BelowMinVer'),
pytest.param(android_support.ANDROID_NDK_MAX_PLATFORM+1, [21, 22, 24], '15.2.4203891', None, True, id='preNdk19AboveMaxVer'),
pytest.param(25, [21, 22, 24], '19.2.4203891', {'23': 21}, True, id='postNdk19NoMatch')
]
)
def test_verify_android_ndk_success(tmpdir, desired_ndk_version_number, available_ndk_revisions, pkg_revision, mappings, expect_error):
test_android_path = 'android_ndk'
for ndk_number in available_ndk_revisions:
tmpdir.ensure(f'{test_android_path}/platforms/android-{ndk_number}/arch-arm64/usr/lib/libc.so')
tmpdir.ensure(f'{test_android_path}/source.properties')
test_ndk_source_properties_file = tmpdir / test_android_path / 'source.properties'
test_ndk_source_properties_file.write_text(f'Pkg.Desc = Android NDK\nPkg.Revision = {pkg_revision}\n', encoding='ASCII')
if mappings:
platform_mapping = {
# min and max are arbitrary for now since we dont use it during evaluation, but if we do, parameterize it here as well
"min": 16, #
"max": 29,
"aliases": {}
}
for key, value in mappings.items():
platform_mapping['aliases'][key] = value
tmpdir.ensure(f'{test_android_path}/meta/platforms.json')
platform_mapping_file = tmpdir / test_android_path / 'meta/platforms.json'
platform_mapping_file.write_text(json.dumps(platform_mapping), encoding='ASCII')
input_ndk_path = tmpdir.join(test_android_path).realpath()
try:
android_ndk_platform_number, android_ndk_path = android_support.verify_android_ndk(android_ndk_platform=str(desired_ndk_version_number),
argument_name="--android-ndk",
override_android_ndk_path=input_ndk_path)
assert not expect_error
assert android_ndk_platform_number == desired_ndk_version_number
assert android_ndk_path == input_ndk_path
except Exception:
assert expect_error
+10
View File
@@ -0,0 +1,10 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
@@ -0,0 +1,68 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
import argparse
import os
import pathlib
import sys
SCHEME_NAME = 'AzTestRunner'
# 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)
from cmake.Tools import common
def build_ios_test(build_dir, configuration):
build_path = pathlib.Path(build_dir) if os.path.isabs(build_dir) else pathlib.Path(ROOT_DEV_PATH) / build_dir
if not build_path.is_dir():
raise common.LmbrCmdError(f"Invalid build directory '{str(build_path)}'")
xcode_build = common.CommandLineExec('/usr/bin/xcodebuild')
command_line_arguments = ['build-for-testing',
'-project', 'Lumberyard.xcodeproj',
'-scheme', SCHEME_NAME,
'-configuration', configuration]
xcode_out = xcode_build.popen(command_line_arguments, cwd=build_path, shell=False)
while xcode_out.poll() is None:
print(xcode_out.stdout.readline())
def main(args):
parser = argparse.ArgumentParser(description="Launch a test module on a target iOS device.")
parser.add_argument('-b', '--build-dir',
help='The relative build directory to deploy from.',
required=True)
parser.add_argument('-c', '--configuration',
help='The build configuration from the build directory for the source deployment files',
default='profile')
parsed_args = parser.parse_args(args)
build_ios_test(build_dir=parsed_args.build_dir,
configuration=parsed_args.configuration)
return 0
if __name__ == '__main__':
try:
result_code = main(sys.argv[1:])
exit(result_code)
except common.LmbrCmdError as err:
logging.error(str(err))
exit(err.code)
+165
View File
@@ -0,0 +1,165 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
import argparse
import glob
import logging
import os
import pathlib
import plistlib
import sys
TEST_TARGET_NAME = 'TestLauncherTarget'
TEST_STARTED_STRING = 'TEST STARTED'
TEST_SUCCESS_STRING = 'TEST SUCCEEDED'
TEST_FAILURE_STRING = 'TEST FAILED'
# 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)
from cmake.Tools import common
def launch_ios_test(build_dir, target_dev_name, test_target, timeout_secs, test_filter, xctestrun_file):
build_path = pathlib.Path(build_dir) if os.path.isabs(build_dir) else pathlib.Path(ROOT_DEV_PATH) / build_dir
if not build_path.is_dir():
raise common.LmbrCmdError(f"Invalid build directory '{str(build_path)}'")
if xctestrun_file:
xctestrun_file_path = build_path / xctestrun_file
if not os.path.exists(xctestrun_file_path):
raise common.LmbrCmdError(f"'{str(xctestrun_file_path)}' not found in '{str(build_path)}'.")
else:
test_run_file = xctestrun_file_path
else:
# By default, xcodebuild will place the xctestrun file at the root of the build directory. There is only one file.
# The xctestrun filename has the format <scheme_name>_iphoneos<sdk_version>-<arch>.xctestrun
# Our scheme name is always "AzTestRunner" and the only iOS architecture we support is arm64.
# The SDK version is the only variable. But, Xcode only allows the installation of the latest SDK(based on Xcode version).
glob_pattern = str(build_path) + '/AzTestRunner_iphoneos*-arm64.xctestrun'
test_run_files = glob.glob(glob_pattern)
if not test_run_files:
raise common.LmbrCmdError(f"No xctestrun file found in '{str(build_path)}'. Run build_ios_test.py first.")
test_run_file = test_run_files[0]
test_targets = common.get_validated_test_modules(test_modules=test_target, build_dir_path=build_path)
test_run_contents = []
with open(test_run_file, 'rb') as fp:
test_run_contents = plistlib.load(fp)
xcode_build = common.CommandLineExec('/usr/bin/xcodebuild')
for target in test_targets:
with open(test_run_file, 'wb') as fp:
fp.truncate(0)
command_line_arguments = [target, 'AzRunUnitTests']
if test_filter:
command_line_arguments.extend(['gtest_filter', test_filter])
test_run_contents[TEST_TARGET_NAME]['CommandLineArguments'] = command_line_arguments
with open(test_run_file, 'wb') as fp:
plistlib.dump(test_run_contents, fp, sort_keys=False)
xcode_args = ['test-without-building', '-xctestrun', test_run_file, '-destination', f'platform=iOS,name={target_dev_name}']
if timeout_secs < 0:
xcode_args.extend(['-test-timeouts-enabled', 'NO'])
else:
xcode_args.extend(['-test-timeouts-enabled', 'YES'])
xcode_args.extend(['-maximum-test-execution-time-allowance', f'{timeout_secs}'])
xcode_out = xcode_build.popen(xcode_args, cwd=build_path, shell=False)
# Log XCTest's output to debug.
# Use test start and end markers to separate XCTest output from AzTestRunner's output
test_success = False
test_output = False
while xcode_out.poll() is None:
line = xcode_out.stdout.readline()
if TEST_STARTED_STRING in line:
test_output = True
if TEST_SUCCESS_STRING in line:
test_success = True
test_output = False
elif TEST_FAILURE_STRING in line:
test_output = False
if test_output:
print(line)
else:
logging.debug(line)
print(f'{target} Succeeded') if test_success else print(f'{target} Failed')
def main(args):
parser = argparse.ArgumentParser(description="Launch a test module on a target iOS device.")
parser.add_argument('-b', '--build-dir',
help='The relative build directory to deploy from.',
required=True)
parser.add_argument('test_module',
nargs='*',
help='The test module(s) to launch on the target device. Defaults to all registered test modules',
default=[])
parser.add_argument('--device-name',
help='The name of the iOS device on which to launch. Defaults to first connected device found.',
required=True)
parser.add_argument('--timeout',
help='The timeout in secs for each test module to prevent deadlocked tests',
type=int,
default=-1)
parser.add_argument('--test-filter',
help='Optional gtest filter to pass along to the unit test launcher',
default=None)
parser.add_argument('--xctestrun-file',
help='Optional parameter to specify custom xctestrun file (path relative to build directory)',
default=None)
parser.add_argument('--debug',
help='Enable debug logging',
action='store_true')
parsed_args = parser.parse_args(args)
logging.basicConfig(format='%(levelname)s: %(message)s',
level=logging.DEBUG if parsed_args.debug else logging.INFO)
result = launch_ios_test(build_dir=parsed_args.build_dir,
target_dev_name=parsed_args.device_name,
test_target=parsed_args.test_module,
timeout_secs=int(parsed_args.timeout),
test_filter=parsed_args.test_filter,
xctestrun_file=parsed_args.xctestrun_file)
return 0 if result else 1
if __name__ == '__main__':
try:
result_code = main(sys.argv[1:])
exit(result_code)
except common.LmbrCmdError as err:
logging.error(str(err))
exit(err.code)
+10
View File
@@ -0,0 +1,10 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
+293
View File
@@ -0,0 +1,293 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
import argparse
import logging
import os
import sys
from cmake.Tools import utils
from cmake.Tools import common
logger = logging.getLogger()
logging.basicConfig()
def add_gem_dependency(cmake_file: str,
gem_name: str) -> int:
"""
adds a gem dependency to a cmake file
:param cmake_file: path to the cmake file
:param gem_name: name of the gem to add
:return: 0 for success or non 0 failure code
"""
if not os.path.isfile(cmake_file):
logger.error(f'Failed to locate cmake file {cmake_file}')
return 1
if not utils.validate_identifier(gem_name):
logger.error(f'{gem_name} is not a valid gem name')
return 1
# on a line by basis, see if there already is Gem::{gem_name}
# find the first occurrence of a gem, copy its formatting and replace
# the gem name with the new one and append it
# if the gem is already present fail
t_data = []
added = False
with open(cmake_file, 'r') as s:
for line in s:
if f'Gem::{gem_name}' in line:
logger.warning(f'{gem_name} is already a gem dependency.')
return 0
if not added and r'Gem::' in line:
new_gem = ' ' * line.find(r'Gem::') + f'Gem::{gem_name}\n'
t_data.append(new_gem)
added = True
t_data.append(line)
# if we didn't add it the set gem dependencies could be empty so
# add a new gem, if empty the correct format is 1 tab=4spaces
if not added:
index = 0
for line in t_data:
index = index + 1
if r'set(GEM_DEPENDENCIES' in line:
t_data.insert(index, f' Gem::{gem_name}\n')
added = True
break
if not added:
logger.error(f'{cmake_file} is malformed.')
return 1
# write the cmake
os.unlink(cmake_file)
with open(cmake_file, 'w') as s:
s.writelines(t_data)
return 0
def remove_gem_dependency(cmake_file: str,
gem_name: str) -> int:
"""
removes a gem dependency form a cmake file
:param cmake_file: path to the cmake file
:param gem_name: name of the gem to remove
:return: 0 for success or non 0 failure code
"""
if not os.path.isfile(cmake_file):
logger.error(f'Failed to locate cmake file {cmake_file}')
return 1
# on a line by basis, remove any line with Gem::{gem_name}
t_data = []
# Remove the gem from the cmake_dependencies file by skipping the gem name entry
removed = False
with open(cmake_file, 'r') as s:
for line in s:
if f'Gem::{gem_name}' in line:
removed = True
else:
t_data.append(line)
if not removed:
logger.error(f'Failed to remove Gem::{gem_name} from cmake file {cmake_file}')
return 1
# write the cmake
os.unlink(cmake_file)
with open(cmake_file, 'w') as s:
s.writelines(t_data)
return 0
def add_remove_gem(add: bool,
dev_root: str,
gem_path: str,
project_path: str,
runtime_dependency: bool = True,
tool_dependency: bool = True) -> int:
"""
add a gem to a project
:param add: should we add a gem, if false we remove a gem
:param dev_root: the dev root of the engine
:param gem_path: path to the gem to add
:param project_path: path to the project to add the gem to
:param runtime_dependency: optional bool to specify this is a runtime gem for the game, default is true
:param tool_dependency: optional bool to specify this is a tool gem for the editor, default is true
:return: 0 for success or non 0 failure code
"""
# if no dev root error
if not dev_root:
logger.error('Dev root cannot be empty.')
return 1
dev_root = dev_root.replace('\\', '/')
# if no project path error
if not project_path:
logger.error('Project path cannot be empty.')
return 1
project_path = project_path.replace('\\', '/')
# if no gem path error
if not gem_path:
logger.error('Gem path cannot be empty.')
return 1
gem_path = gem_path.replace('\\', '/')
# gem path can be absolute or relative to the Gems folder under the dev root
if os.path.isabs(gem_path):
gem_root = gem_path
else:
gem_root = f'{dev_root}/Gems/{gem_path}'
# make sure this gem already exists
if not os.path.isdir(gem_root):
logger.error(f'{gem_root} dir does not exist.')
return 1
# gem name is now the last component of the gem root
gem_name = os.path.basename(gem_root)
if not utils.validate_identifier(gem_name):
logger.error(f'{gem_name} is not a valid Gem name.')
return 1
# if the project path is absolute use it, if not make it relative to the dev root
if os.path.isabs(project_path):
project_root = project_path
else:
project_root = f'{dev_root}/{project_path}'
# make sure this project already exists
if not os.path.isdir(project_root):
logger.error(f'Project specified at {project_root} dir does not exist.')
return 1
# if the user has not specified either we will assume they meant both
if not runtime_dependency and not tool_dependency:
runtime_dependency = True
tool_dependency = True
ret_val = 0
if runtime_dependency:
# make sure this is a project has a runtime_dependencies.cmake file
project_runtime_dependencies_file = f'{project_root}/Gem/Code/runtime_dependencies.cmake'
if not os.path.isfile(project_runtime_dependencies_file):
logger.error(f'Runtime dependencies file {project_runtime_dependencies_file} is not present.')
return 1
if add:
# add the dependency
ret_val = add_gem_dependency(project_runtime_dependencies_file, gem_name)
else:
# remove the dependency
ret_val = remove_gem_dependency(project_runtime_dependencies_file, gem_name)
if ret_val == 0 and tool_dependency:
# make sure this is a project has a tool_dependencies.cmake file
project_tool_dependencies_file = f'{project_root}/Gem/Code/tool_dependencies.cmake'
if not os.path.isfile(project_tool_dependencies_file):
logger.error(f'Tool dependencies file {project_tool_dependencies_file} is not present.')
return 1
if add:
# add the dependency
ret_val = add_gem_dependency(project_tool_dependencies_file, gem_name)
else:
# remove the dependency
ret_val = remove_gem_dependency(project_tool_dependencies_file, gem_name)
return ret_val
def _run_add_gem(args: argparse) -> int:
return add_remove_gem(True,
common.determine_dev_root(),
args.gem_path,
args.project_path,
args.runtime_dependency,
args.tool_dependency)
def _run_remove_gem(args: argparse) -> int:
return add_remove_gem(False,
common.determine_dev_root(),
args.gem_path,
args.project_path,
args.runtime_dependency,
args.tool_dependency)
def add_args(parser, subparsers) -> None:
"""
add_args is called to add expected parser arguments and subparsers arguments to each command such that it can be
invoked locally or aggregated by a central python file.
Ex. Directly run from this file alone with: python add_remove_gem.py add_gem --gem TestGem --project TestProject
OR
lmbr.py can aggregate commands by importing add_remove_gem, call add_args and
execute: python lmbr.py add_gem --gem TestGem --project TestProject
:param parser: the caller instantiates a parser and passes it in here
:param subparsers: the caller instantiates subparsers and passes it in here
"""
add_gem_subparser = subparsers.add_parser('add_gem')
add_gem_subparser.add_argument('-pp', '--project-path', required=True,
help='The path to the project, can be absolute or dev root relative')
add_gem_subparser.add_argument('-gp', '--gem-path', required=True,
help='The path to the gem, can be absolute or dev root/Gems relative')
add_gem_subparser.add_argument('-rd', '--runtime-dependency',
help='Optional toggle if this gem should only be added as a runtime dependency'
' and not tool dependency. If neither is specified then the gem is added'
' to both.')
add_gem_subparser.add_argument('-td', '--tool-dependency',
help='Optional toggle if this gem should only be added as a tool dependency'
' and not a runtime dependency. If neither is specified then the gem is'
' added to both.')
add_gem_subparser.set_defaults(func=_run_add_gem)
remove_gem_subparser = subparsers.add_parser('remove_gem')
remove_gem_subparser.add_argument('-pp', '--project-path', required=True,
help='The path to the project, can be absolute or dev root relative')
remove_gem_subparser.add_argument('-gp', '--gem-path', required=True,
help='The path to the gem, can be absolute or dev root/Gems relative')
remove_gem_subparser.add_argument('-rd', '--runtime-dependency',
help='Optional toggle if this gem should only be removed as a runtime dependency'
' and not tool dependency. If neither is specified then the gem is removed'
' from both.')
remove_gem_subparser.add_argument('-td', '--tool-dependency',
help='Optional toggle if this gem should only be removed as a tool dependency'
' and not a runtime dependency. If neither is specified then the gem is'
' removed from both.')
remove_gem_subparser.set_defaults(func=_run_remove_gem)
if __name__ == "__main__":
# parse the command line args
the_parser = argparse.ArgumentParser()
# add subparsers
the_subparsers = the_parser.add_subparsers(help='sub-command help')
# add args to the parser
add_args(the_parser, the_subparsers)
# parse args
the_args = the_parser.parse_args()
# run
ret = the_args.func(the_args)
# return
sys.exit(ret)
+689
View File
@@ -0,0 +1,689 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
import configparser
import hashlib
import logging
import json
import os
import re
import shutil
import stat
import string
import subprocess
import pathlib
import platform
from subprocess import CalledProcessError
from distutils.version import LooseVersion
from cmake.Tools import layout_tool
# Text encoding Constants for reading/writing to files.
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__), '..', '..', '..', '..'))
if platform.system() == 'Windows':
# Re-use microsoft error codes since this script is meant to only run on windows host platforms
ERROR_CODE_FILE_NOT_FOUND = 2
ERROR_CODE_ERROR_NOT_SUPPORTED = 50
ERROR_CODE_INVALID_PARAMETER = 87
ERROR_CODE_CANNOT_COPY = 266
ERROR_CODE_ERROR_DIRECTORY = 267
else:
# Posix does not match any of the following errors to specific codes, so just the standard '1'
ERROR_CODE_FILE_NOT_FOUND = 1
ERROR_CODE_ERROR_NOT_SUPPORTED = 1
ERROR_CODE_INVALID_PARAMETER = 1
ERROR_CODE_CANNOT_COPY = 1
ERROR_CODE_ERROR_DIRECTORY = 1
# Specific error codes that we cant match to any platform code, so just set to the standard '1'
ERROR_CODE_ENVIRONMENT_ERROR = 1
ERROR_CODE_GENERAL_ERROR = 1
DEV_ROOT_CHECK_FILE = 'engine.json'
HASH_CHUNK_SIZE = 200000
class LmbrCmdError(Exception):
"""
Wrapper class to the general exception class where will absorb and prevent the printing of stack.
We will rely on specific error conditions instead.
"""
def __init__(self, msg, code=ERROR_CODE_GENERAL_ERROR):
"""
Init the class
:param msg: The detailed error message to print out
:param code: The return code to return from the command line execution
"""
self.msg = msg
self.code = code
def __str__(self):
return str(self.msg)
def determine_dev_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
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
:return: The root path that is validated to contain engine.json if found, None if not found
"""
current_path = os.path.normpath(starting_path or os.getcwd())
check_file = os.path.join(current_path, DEV_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)
current_path = next_path
if not os.path.isfile(check_file):
return None
return current_path
def get_config_file_values(config_file_path, keys_to_extract):
"""
Read a lumberyard config file and extract specific keys if they are set
@param config_file_path: The lumberyard config file to parse
@param keys_to_extract: The specific keys to lookup the values if set
@return: Dictionary of keys and its values (for matched keys)
"""
result_map = {}
with open(config_file_path, 'r') as config_file:
bootstrap_contents = config_file.read()
for search_key in keys_to_extract:
search_result = re.search(r'^\s*{}\s*=\s*([\w\.]+)'.format(search_key), bootstrap_contents, re.MULTILINE)
if search_result:
result_value = search_result.group(1)
result_map[search_key] = result_value
return result_map
def get_bootstrap_values(dev_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 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')
if not os.path.isfile(bootstrap_file):
raise LmbrCmdError("Missing 'bootstrap.cfg' file from dev root ('{}')".format(dev_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):
"""
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 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')
if not os.path.isfile(ap_config_file):
raise LmbrCmdError("Missing required asset processor configuration file at '{}'".format(dev_root),
ERROR_CODE_FILE_NOT_FOUND)
parser = configparser.ConfigParser()
parser.read([ap_config_file])
if parser.has_option('Platforms', bootstrap_asset_type):
enabled_value = parser.get('Platforms', bootstrap_asset_type)
else:
# If 'pc' is not set, then default it to 'disable'. For other platforms, their default is 'disabled'
enabled_value = 'disabled' if bootstrap_asset_type != 'pc' else 'enabled'
return enabled_value == 'enabled'
def file_fingerprint(path, deep_check=False):
"""
Calculate a file hash for for a file from either its metadata or its content (deep_check=True)
:param path: The absolute path to the file to check its fingerprint. (Does not work on directories)
:param deep_check: Flag to use a deep check (hash of the entire file content) or just its metadata (timestamp+size)
:return: The hash
"""
if os.path.isdir(path):
raise LmbrCmdError("Cannot fingerprint '{}' because its a directory".format(path),
ERROR_CODE_ERROR_DIRECTORY)
# Use MD5 hash
hasher = hashlib.md5()
# Always start with a shallow check: Start the hash by hashing the mod-time and file size
path_file_stat = os.stat(path)
hasher.update(str(path_file_stat.st_mtime).encode('UTF-8'))
hasher.update(str(path_file_stat.st_size).encode('UTF-8'))
# If doing a deep check, also include the contents
if deep_check:
with open(path, 'rb') as file_to_hash:
while True:
content = file_to_hash.read(HASH_CHUNK_SIZE)
hasher.update(content)
if not content:
break
return hasher.hexdigest()
def load_template_file(template_file_path, template_env):
"""
Helper method to load in a template file and return the processed template based on the input template environment
This will also handle '###' tokens to strip out of the final output completely to support things like adding
copyrights to the template that is not intended for the output text
:param template_file_path: The path to the template file to load
:param template_env: The template environment dictionary for the template file to process
:return: The processed content from the template file
:raises: FileNotFoundError: If the template file path cannot be found
"""
try:
template_file_content = template_file_path.resolve(strict=True).read_text(encoding=DEFAULT_TEXT_READ_ENCODING,
errors=ENCODING_ERROR_HANDLINGS)
# Filter out all lines that start with '###' before replacement
filtered_template_file_content = (str(re.sub('###.*', '', template_file_content)).strip())
return string.Template(filtered_template_file_content).substitute(template_env)
except FileNotFoundError:
raise FileNotFoundError(f"Invalid file path. Cannot find template file located at {str(template_file_path)}")
def verify_tool(override_tool_path, tool_name, tool_filename, argument_name, tool_version_argument, tool_version_regex, min_version, max_version):
"""
Support method to validate a required system tool needed for the build either through an installed tool in the
environment path, or an override path provided
:param override_tool_path: The override path to use to locate the tool's binary. If not provided, the rely on the fact that the tool is in the PATH enviroment
:param tool_name: The name of the tool to present to the output
:param tool_filename: The filename to use to search for when the override path is provided
:param argument_name: The name of the command line argument to display to the user if needed
:param tool_version_argument: The argument that the tool expects when querying for the version
:param tool_version_regex: The regex used to parse the version number from the 'version' argument
:param min_version: Optional min_version to validate against. (None to skip min version validation)
:param max_version: Optional max_version to validate against. (None to skip max version validation)
:return: Tuple of the resolved tool version and the resolved override tool path if provided
"""
try:
# Use either the provided gradle override or the gradle in the path environment
if override_tool_path:
# The path can be either to an install base folder or its actual 'bin' folder
if isinstance(override_tool_path, str):
override_tool_path = pathlib.Path(override_tool_path)
elif not isinstance(override_tool_path, pathlib.Path):
raise LmbrCmdError(f"Invalid {tool_name} path argument. '{override_tool_path}' must be a string or Path",
ERROR_CODE_INVALID_PARAMETER)
check_tool_path = override_tool_path / tool_filename
if not check_tool_path.is_file():
check_tool_path = pathlib.Path(override_tool_path) / 'bin' / tool_filename
if not check_tool_path.is_file():
raise LmbrCmdError(f"Invalid {tool_name} path argument. '{override_tool_path}' is not a valid {tool_name} path",
ERROR_CODE_INVALID_PARAMETER)
resolved_override_tool_path = str(check_tool_path.resolve())
tool_source = str(check_tool_path.resolve())
tool_desc = f"{tool_name} path provided in the command line argument '{argument_name}={override_tool_path}' "
else:
resolved_override_tool_path = None
tool_source = tool_name
tool_desc = "installed gradle in the system path"
# Extract the version and verify
version_output = subprocess.check_output([tool_source, tool_version_argument],
shell=True).decode(DEFAULT_TEXT_READ_ENCODING,
ENCODING_ERROR_HANDLINGS)
version_match = tool_version_regex.search(version_output)
if not version_match:
raise RuntimeError()
result_version = LooseVersion(str(version_match.group(1)).strip())
if min_version and result_version < min_version:
raise LmbrCmdError(f"The {tool_desc} does not meet the minimum version of gradle required ({str(min_version)}).",
ERROR_CODE_ENVIRONMENT_ERROR)
elif max_version and result_version > max_version:
raise LmbrCmdError(f"The {tool_desc} exceeds maximum version of gradle supported ({str(max_version)}).",
ERROR_CODE_ENVIRONMENT_ERROR)
return result_version, resolved_override_tool_path
except (CalledProcessError, WindowsError, RuntimeError) as e:
logging.error(f"Call to '{tool_source}' resulted in error: {e}")
raise LmbrCmdError(f"{tool_name} cannot be resolved or there was a problem determining its version number. "
f"Either make sure its in the system path environment or a valid path is passed in "
f"through the {argument_name} argument.",
ERROR_CODE_ERROR_NOT_SUPPORTED)
def verify_game_project_and_dev_root(game_name, dev_root):
"""
Verify the dev root folder and the game name against that dev root. This will perform basic minimal checks
for validation:
1. Make sure bootstrap.cfg exists
2. Make sure ${dev_root}/${game_name}/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
: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
"""
dev_root_path = pathlib.Path(dev_root)
if not dev_root_path.exists():
raise LmbrCmdError(f"Invalid dev root path ({dev_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",
ERROR_CODE_INVALID_PARAMETER)
if game_name is None:
return None, dev_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}",
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)
def remove_dir_path(path):
"""
Helper function to delete a folder, ignoring all errors if possible
:param path: The Path to the folder to delete
"""
if not path.is_dir() and path.is_file():
# Avoid accidentally deleting a file
raise RuntimeError("Cannot perform 'remove_dir_path' on file {path}. It must be a directory.")
if path.exists():
files_to_delete = []
for root, dirs, files in os.walk(path):
for file in files:
files_to_delete.append(os.path.join(root, file))
for file in files_to_delete:
os.chmod(file, stat.S_IWRITE)
os.remove(file)
shutil.rmtree(path.resolve(), ignore_errors=True)
def normalize_path_for_settings(path, escape_drive_sep=False):
"""
Normalize a path for a settings file in case backslashes are treated as escape characters
:param path: The path to process (string or pathlib.Path)
:param escape_drive_sep: Option to escape any ':' driver separator (windows)
:return: The normalized path
"""
if isinstance(path, str):
processed = path
else:
processed = str(path.resolve())
processed = processed.replace('\\', '/')
if escape_drive_sep:
processed = processed.replace(':', '\\:')
return processed
def wrap_parsed_args(parsed_args):
"""
Function to add a method to the parsed argument object to transform a long-form argument name to and get the
parsed values based on the input long form.
This will allow us to read an argument like '--foo-bar=Orange' by using the built in method rather than looking for
the argparsed transformed attrobite 'foo_bar'
:param parsed_args: The parsed args object to wrap
"""
def parse_argument_attr(argument):
argument_attr = argument[2:].replace('-', '_')
return getattr(parsed_args, argument_attr)
parsed_args.get_argument = parse_argument_attr
class PlatformSettings(object):
"""
Platform settings reader
This will generate a simple settings object based on the cmake generated 'platform.settings' file
generated by cmake/FileUtil.cmake
"""
def __init__(self, build_dir):
platform_last_file = build_dir / 'platform.settings'
if not platform_last_file.exists():
raise LmbrCmdError(f"Invalid build directory {build_dir}. Missing 'platform.settings'.")
config = configparser.ConfigParser()
config.read(platform_last_file)
# Look up the general common settings across all platforms
projects_str = config['settings']['game_projects']
if projects_str:
self.projects = projects_str.split(';')
asset_deploy_mode = config['settings'].get('asset_deploy_mode')
self.asset_deploy_mode = asset_deploy_mode if asset_deploy_mode else None
asset_deploy_type = config['settings'].get('asset_deploy_type')
self.asset_deploy_type = asset_deploy_type if asset_deploy_type else None
self.override_pak_root = config['settings'].get('override_pak_root', '')
# Apply all platform-specific settings under the '[<platform_name>]' section in the config file
platform_name = config['settings']['platform']
if platform_name in config.sections():
platform_items = config.items(platform_name)
for platform_item_key, platform_item_value in platform_items:
# Prevent any custom platform setting to overwrite a common one
if platform_item_key in ('asset_deploy_mode', 'asset_deploy_type', 'projects'):
logging.warning(f"Reserved key '{platform_item_key}' found in platform section of {str(platform_last_file)}. Ignoring")
continue
setattr(self, platform_item_key, platform_item_value)
def validate_build_dir_and_config(build_dir_name, configuration):
"""
Validate the build directory and configuration. The current working directory must be the engine root
:param build_dir_name: The name of the build directory
:param configuration: The configuration name (debug, profile, or release)
:return: tuple of pathlibs for the build directory, and the configuration directory
"""
build_dir = pathlib.Path(os.getcwd()) / build_dir_name
if not build_dir.is_dir():
raise LmbrCmdError(f"Invalid build directory {build_dir_name}")
build_config_dir = build_dir / 'bin' / configuration
if not build_config_dir.is_dir():
raise LmbrCmdError(f"Output path for build configuration {configuration} not found. Make sure that it was built.")
return build_dir, build_config_dir
def validate_deployment_arguments(build_dir_name, configuration, game_name):
"""
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
@return: Tuple of (resolved build_dir, game name, asset mode, asset_type, and Pak root folder)
"""
build_dir, build_config_dir = validate_build_dir_and_config(build_dir_name, configuration)
platform_settings = PlatformSettings(build_dir)
if not game_name:
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")
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}.")
return build_dir, game_name, platform_settings.asset_deploy_mode, platform_settings.asset_deploy_type, platform_settings.override_pak_root or DEFAULT_PAK_ROOT
class CommandLineExec(object):
def __init__(self, executable_path):
if not os.path.isfile(executable_path):
raise LmbrCmdError(f"Invalid command-line executable '{executable_path}'")
self.executable_path = executable_path
def exec(self, arguments, capture_stdout=False, suppress_stderr=False, cwd=None):
"""
Wrapper to executing calls
@param arguments: Arguments to pass to ''
@param capture_stdout: If true, capture the stdout of the command to the result object. Enable this if you need the results of the call to continue a workflow
@param suppress_stderr: If true, suppress capturing the stderr stream
@param cwd: Specify an optional current working directory for the execution
@return: Tuple of the exit code from command line executable, the stdout (if capture_stdout is set to True), and the stderr if any
"""
try:
call_args = [self.executable_path]
if isinstance(arguments, list):
call_args.extend(arguments)
else:
call_args.append(str(arguments))
logging.debug("exec(%s)", subprocess.list2cmdline(call_args))
result = subprocess.run(call_args,
shell=True,
capture_output=capture_stdout,
stderr=subprocess.DEVNULL if not capture_stdout and suppress_stderr else None,
encoding='utf-8',
errors='ignore',
cwd=cwd)
result_code = result.returncode
result_stdout = result.stdout
result_stderr = None if suppress_stderr else result.stderr
return result_code, result_stdout, result_stderr
except subprocess.CalledProcessError as err:
raise LmbrCmdError(f"Error trying to call '{self.executable_path}': {str(err)}")
def popen(self, arguments, cwd=None, shell=True):
"""
Wrapper to executing calls
@param arguments: Arguments to pass to ''
@param capture_stdout: If true, capture the stdout of the command to the result object. Enable this if you need the results of the call to continue a workflow
@param cwd: Specify an optional current working directory for the execution
@return: Tuple of the exit code from command line executable, the stdout (if capture_stdout is set to True), and the stderr if any
"""
try:
call_args = [self.executable_path]
if isinstance(arguments, list):
call_args.extend(arguments)
else:
call_args.append(str(arguments))
logging.debug("exec(%s)", subprocess.list2cmdline(call_args))
result = subprocess.Popen(call_args,
universal_newlines=True,
bufsize=1,
shell=shell,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
encoding='utf-8',
errors='ignore',
cwd=cwd)
return result
except subprocess.CalledProcessError as err:
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):
"""
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 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,
asset_type=asset_type,
warning_on_missing_assets=True,
layout_target=layout_root,
override_pak_folder=None,
copy=False)
def get_cmake_dependency_modules(build_dir_path, target, module_type):
"""
Read a dependency registry file for a particular target and get the modules defined for that target.
If the file does not exist, that means means the either target is not configured, or the special test
runner setreg is not generated because it is not generated in monolithic mode
:param build_dir_path: The build directory (Pathlib) base directory
:param target: The name of the target the dependency file is registered for
:param module_type: The module type to query for
:return: List of modules that is registered for the target. An empty list if there is no dependencies for a target or the registry setting isnt set
"""
dep_modules = []
try:
cmake_dep_path = build_dir_path / 'Registry' / f'cmake_dependencies.{target.lower()}.setreg'
if not cmake_dep_path.is_file():
return dep_modules
with cmake_dep_path.open() as cmake_dep_json_file:
cmake_dep_json = json.load(cmake_dep_json_file)
test_module_items = cmake_dep_json['Amazon'][module_type]
for _, test_module_item in test_module_items.items():
module_file = test_module_item['Module']
dep_modules.append(module_file)
except FileNotFoundError:
raise LmbrCmdError(f'{target} registry not found')
except (KeyError, json.JSONDecodeError) as err:
raise LmbrCmdError(f'AzTestRunner registry issue: {str(err)}')
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
:param build_dir_path: The target build directory (Pathlib) base directory
:return: List of modules that is registered for the target. An empty list if there is no dependencies for a target or the registry setting isnt set
"""
dep_modules = []
try:
unit_test_module_path = build_dir_path / 'unit_test_modules.json'
with unit_test_module_path.open() as unit_test_json_file:
unit_test_json = json.load(unit_test_json_file)
test_module_items = unit_test_json['Amazon']
for _, test_module_item in test_module_items.items():
module_file = test_module_item['Module']
dep_modules.append(module_file)
except FileNotFoundError:
raise LmbrCmdError(f"Unit test registry not found ('{str(unit_test_module_path)}')")
except (KeyError, json.JSONDecodeError) as err:
raise LmbrCmdError(f'Unit test registry file issue: {str(err)}')
return dep_modules
def get_validated_test_modules(test_modules, build_dir_path):
"""
Validatate the provided test modules against all test modules
:param test_modules: List of test target names
:param build_dir_path: The target build directory (Pathlib) base directory
:return: List of valid test modules that match the input test modules. If the input test modules is an empty list, return all valid test modules
"""
# Collect the test modules that can be launched
all_test_modules = get_test_module_registry(build_dir_path=build_dir_path)
validated_test_modules = []
# Validate input test targets or use all test modules if no specific test target is supplied
if test_modules:
assert isinstance(test_modules, list)
for test_target_check in test_modules:
if test_target_check not in all_test_modules:
raise LmbrCmdError(f"Invalid test module {test_target_check}")
validated_test_modules.append(test_target_check)
else:
validated_test_modules = all_test_modules
return validated_test_modules
+117
View File
@@ -0,0 +1,117 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
import argparse
import logging
import os
import sys
import re
from cmake.Tools import common
logger = logging.getLogger()
logging.basicConfig()
def set_current_project(dev_root: str,
project_path: str) -> int:
"""
set what the current project is
:param dev_root: the dev root of the engine
:param project_path: the path of the project you want to set
:return: 0 for success or non 0 failure code
"""
project_path = project_path.strip()
if not project_path.isalnum():
logger.error('Project Name invalid. Set current project failed.')
return 1
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, flags=re.IGNORECASE)
if os.path.isfile(os.path.join(dev_root, 'bootstrap.cfg')):
os.unlink(os.path.join(dev_root, 'bootstrap.cfg'))
with open(os.path.join(dev_root, 'bootstrap.cfg'), 'w') as s:
s.write(data)
except Exception as e:
logger.error('Set current project failed.' + str(e))
return 1
return 0
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
"""
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()
except Exception as e:
logger.error('Failed to get current project. Exception: ' + str(e))
return ''
return sys_game_folder
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)
return 0
return 1
def _run_set_current_project(args: argparse) -> int:
return set_current_project(common.determine_dev_root(), args.project_path)
def add_args(parser, subparsers) -> None:
"""
add_args is called to add expected parser arguments and subparsers arguments to each command such that it can be
invoked locally or aggregated by a central python file.
Ex. Directly run from this file alone with: python current_project.py set_current_project --project TestProject
OR
lmbr.py can aggregate commands by importing current_project, call add_args and
execute: python lmbr.py set_current_project --project TestProject
:param parser: the caller instantiates a parser and passes it in here
:param subparsers: the caller instantiates subparsers and passes it in here
"""
get_current_project_subparser = subparsers.add_parser('get_current_project')
get_current_project_subparser.set_defaults(func=_run_get_current_project)
set_current_project_subparser = subparsers.add_parser('set_current_project')
set_current_project_subparser.add_argument('-pp', '--project-path', required=True,
help='The path to the project, can be absolute or dev root relative')
set_current_project_subparser.set_defaults(func=_run_set_current_project)
if __name__ == "__main__":
# parse the command line args
the_parser = argparse.ArgumentParser()
# add subparsers
the_subparsers = the_parser.add_subparsers(help='sub-command help')
# add args to the parser
add_args(the_parser, the_subparsers)
# parse args
the_args = the_parser.parse_args()
# run
ret = the_args.func(the_args)
# return
sys.exit(ret)
File diff suppressed because it is too large Load Diff
+244
View File
@@ -0,0 +1,244 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
import argparse
import datetime
import logging
import pathlib
import platform
import sys
import os
import subprocess
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)
from cmake.Tools import common
# The location of this python script is not portable relative to the engine root, we determine the engine root based
# on its relative location
DEV_ROOT = os.path.realpath(os.path.join(__file__, '../../..'))
BOOTSTRAP_CFG = os.path.join(DEV_ROOT, 'bootstrap.cfg')
EXECUTABLE_EXTN = '.exe' if platform.system() == 'Windows' else ''
RC_NAME = f'rc{EXECUTABLE_EXTN}'
APB_NAME = f'AssetProcessorBatch{EXECUTABLE_EXTN}'
# Depending on the user request for verbosity, the argument list to subprocess may or may not redirect stdout to NULL
VERBOSE_CALL_ARGS = dict(
shell=True,
cwd=DEV_ROOT
)
NON_VERBOSE_CALL_ARGS = dict(
**VERBOSE_CALL_ARGS,
stdout=subprocess.DEVNULL
)
def command_arg(arg):
"""
Work-around for an issue when running subprocess on Linux: subprocess.check_call will take in the argument as an array
but only invokes the first item in the array, ignoring the arguments. As quick fix, we will combine the array into the
full command line and execute it that way on non-windows platforms
"""
if platform.system() == 'Windows':
return arg
else:
return ' '.join(arg)
def validate(binfolder, game_name, pak_script):
#
# Validate the binfolder is relative and contains 'rc' and 'AssetProcessorBatch'
#
if os.path.isabs(binfolder):
raise common.LmbrCmdError("Invalid value for '-b/--binfolder'. It must be a path relative to the engine root folder",
common.ERROR_CODE_ERROR_DIRECTORY)
binfolder_abs_path = pathlib.Path(DEV_ROOT) / binfolder
if not binfolder_abs_path.is_dir():
raise common.LmbrCmdError("Invalid value for '-b/--binfolder'. Path does not exist or is not a directory",
common.ERROR_CODE_ERROR_DIRECTORY)
rc_check = binfolder_abs_path / RC_NAME
if not rc_check.is_file():
raise common.LmbrCmdError(f"Invalid value for '-b/--binfolder'. Path does not contain {RC_NAME}",
common.ERROR_CODE_ERROR_DIRECTORY)
apb_check = binfolder_abs_path / APB_NAME
if not apb_check.is_file():
raise common.LmbrCmdError(f"Invalid value for '-b/--binfolder'. Path does not contain {APB_NAME}",
common.ERROR_CODE_ERROR_DIRECTORY)
#
# Validate the game name represents a game project within the game engine
#
gamefolder_abs_path = pathlib.Path(DEV_ROOT) / game_name
if not gamefolder_abs_path.is_dir():
raise common.LmbrCmdError(f"Invalid value for '-g/--game-name'. No game '{game_name} exists.",
common.ERROR_CODE_ERROR_DIRECTORY)
project_json_path = gamefolder_abs_path / 'project.json'
if not project_json_path.is_file():
raise common.LmbrCmdError(
f"Invalid value for '-g/--game-name'. Folder '{game_name} is not a valid game project.",
common.ERROR_CODE_FILE_NOT_FOUND)
if not os.path.isfile(pak_script):
raise common.LmbrCmdError(f'Pak script file {pak_script} does not exist.',
common.ERROR_CODE_FILE_NOT_FOUND)
def process(binfolder, game_name, asset_platform, autorun_assetprocessor, recompress, fastest_compression, target,
pak_script, warn_on_assetprocessor_error, verbose):
logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG if verbose else logging.INFO)
target_path_root_abs = pathlib.Path(DEV_ROOT) / target
if target_path_root_abs.is_file():
raise common.LmbrCmdError(f"Target '{target}' already exists as a file.",
common.ERROR_CODE_GENERAL_ERROR)
os.makedirs(target_path_root_abs.absolute(), exist_ok=True)
target_pak_folder_name = f'{game_name.lower()}_{asset_platform}_paks'
target_pak = target_path_root_abs / target_pak_folder_name
# Prepare the asset processor batch arguments and execute if requested
if autorun_assetprocessor:
ap_executable = os.path.join(binfolder, APB_NAME)
ap_cmd_args = [ap_executable,
f'/gamefolder={game_name}',
f'/platforms={asset_platform}']
logging.debug("Calling {}".format(' '.join(ap_cmd_args)))
try:
logging.info(f"Running {APB_NAME} on {game_name}")
start_time = datetime.datetime.now()
call_args = VERBOSE_CALL_ARGS if verbose else NON_VERBOSE_CALL_ARGS
subprocess.check_call(command_arg(ap_cmd_args),
**call_args)
total_time = datetime.datetime.now() - start_time
logging.info(f"Asset Processing Complete. Elapse: {total_time}")
except subprocess.CalledProcessError:
if warn_on_assetprocessor_error:
logging.warning('AssetProcessorBatch reported errors')
else:
raise common.LmbrCmdError("AssetProcessorBatch has one or more failed assets.",
common.ERROR_CODE_GENERAL_ERROR)
rc_executable = os.path.join(binfolder, RC_NAME)
rc_cmd_args = [rc_executable,
f'/job={pak_script}',
f'/p={asset_platform}',
f'/game={game_name}',
f'/trg={target_pak}']
if recompress:
rc_cmd_args.append('/recompress=1')
if fastest_compression:
rc_cmd_args.append('/use_fastest=1')
logging.debug("Calling {}".format(' '.join(rc_cmd_args)))
try:
logging.info(f"Running {APB_NAME} on {game_name}")
start_time = datetime.datetime.now()
call_args = VERBOSE_CALL_ARGS if verbose else NON_VERBOSE_CALL_ARGS
subprocess.check_call(command_arg(rc_cmd_args),
**call_args)
total_time = datetime.datetime.now() - start_time
logging.info(f"Asset Processing Complete. Elapse: {total_time}")
logging.info(f"Pak files for {game_name} written to {target_pak}")
except subprocess.CalledProcessError as err:
raise common.LmbrCmdError(f"{RC_NAME} returned an error: {str(err)}.",
err.returncode)
def main(args):
parser = argparse.ArgumentParser()
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'])
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'))
parser.add_argument('-p', '--asset-platform',
help='The asset platform type to process')
parser.add_argument('-a', '--autorun-assetprocessor',
help='Option to automatically invoke asset processor batch on the game before generating the pak',
action='store_true')
parser.add_argument('-w', '--warn-on-assetprocessor-error',
help='When -a/--autorun-assetprocessor is specified, warn on asset processor failure rather than aborting the process',
action='store_true')
parser.add_argument('-r', '--recompress',
action='store_true',
help='If present, the ResourceCompiler (RC.exe) will decompress and compress back each PAK file '
'found as they are transferred from the cache folder to the game_pc_pak folder.')
parser.add_argument('-fc', '--fastest-compression',
action='store_true',
help='As each file is being added to its PAK file, they will be compressed across all available '
'codecs (ZLIB, ZSTD and LZ4) and the one with the fastest decompression time will be '
'chosen. The default is to always use ZLIB')
parser.add_argument('--target',
default='Pak',
help='Specify a target folder for the pak files. (Default : Pak)')
parser.add_argument('--pak-script',
default=f'{DEV_ROOT}/{os.path.normpath("Code/Tools/RC/Config/rc/RCJob_Generic_MakePaks.xml")}',
help="The absolute path of the pak script configuration file to use to create the paks.")
parser.add_argument('-v', '--verbose',
help='Enable debug messages',
action='store_true')
parsed = parser.parse_args(args)
validate(binfolder=parsed.binfolder,
game_name=parsed.game_name,
pak_script=parsed.pak_script)
process(binfolder=parsed.binfolder,
game_name=parsed.game_name,
asset_platform=parsed.asset_platform,
autorun_assetprocessor=parsed.autorun_assetprocessor,
recompress=parsed.recompress,
fastest_compression=parsed.fastest_compression,
target=parsed.target,
pak_script=parsed.pak_script,
warn_on_assetprocessor_error=parsed.warn_on_assetprocessor_error,
verbose=parsed.verbose)
if __name__ == '__main__':
try:
if not os.path.isfile(BOOTSTRAP_CFG):
raise common.LmbrCmdError("Invalid dev root, missing bootstrap.cfg.",
common.ERROR_CODE_FILE_NOT_FOUND)
main(sys.argv[1:])
exit(0)
except common.LmbrCmdError as err:
print(str(err), file=sys.stderr)
exit(err.code)
+672
View File
@@ -0,0 +1,672 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
import argparse
import datetime
import hashlib
import logging
import os
import pathlib
import platform
import re
import shutil
import subprocess
import sys
import tempfile
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)
from cmake.Tools import common
LOCAL_HOST = '127.0.0.1'
CACHE_FOLDER_NAME = 'Cache'
ASSET_MODE_PAK = 'PAK'
ASSET_MODE_LOOSE = 'LOOSE'
ASSET_MODE_VFS = 'VFS'
ALL_ASSET_MODES = [ASSET_MODE_PAK, ASSET_MODE_LOOSE, ASSET_MODE_VFS]
PAK_FOLDER_NAME = 'Pak'
# Maintain a list of build configs that only will support PAK mode
PAK_ONLY_BUILD_CONFIGS = ['RELEASE']
# Save the platform system name. In our case, this will be one of:
# Windows
# Linux
# Darwin (Currently 'Darwin' with python 3.7). Should use 'Windows' and 'Linux' first and fallback to Darwin
PLATFORM_NAME = platform.system()
# List of files to blacklist from copying to the layout folder
COPY_ASSET_FILE_GENERAL_BLACKLIST_FILES = [
'aztest_bootstrap.json',
'editor.cfg',
'assetprocessorplatformconfig.ini',
]
def verify_layout(layout_dir, platform_name, game_name, 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 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
"""
def _warn(msg):
logging.warning(msg)
return 1
def _validate_remote_ap(input_remote_ip, input_remote_connect, remote_on_check):
if remote_on_check is None:
# Validate that if '<platform>_connect_to_remote is enabled, that the 'input_remote_ip' is not set to local host
if input_remote_connect == '1' and input_remote_ip == LOCAL_HOST:
return _warn("'bootstrap.cfg' is configured to connect to Asset Processor remotely, but the 'remote_ip' "
" is configured for LOCAL HOST")
else:
if remote_on_check:
# Verify we are set for remote AP connection
if input_remote_ip == LOCAL_HOST:
return _warn(f"'bootstrap.cfg' is not configured for a remote Asset Processor connection (remote_ip={input_remote_ip})")
if input_remote_connect != '1':
return _warn(f"'bootstrap.cfg' is not configured for a remote Asset Processor connection ({platform_name}_connect_to_remote={input_remote_connect}")
else:
# Verify we are disabled for remote AP connection
if input_remote_connect != '0':
return _warn(f"'bootstrap.cfg' is not configured for a remote Asset Processor connection ({platform_name}_connect_to_remote={input_remote_connect}")
return 0
warning_count = 0
platform_name_lower = platform_name.lower()
game_name_lower = game_name.lower()
layout_path = pathlib.Path(layout_dir)
# Validate bootstrap.cfg exists
bootstrap_file = layout_path / 'bootstrap.cfg'
if not bootstrap_file.is_file():
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',
f'{platform_name_lower}_connect_to_remote',
f'{platform_name_lower}_wait_for_connect',
f'{platform_name_lower}_assets',
f'assets',
f'{platform_name_lower}_remote_ip',
f'remote_ip'
])
# Validate the system_{platform}_{asset type}.cfg exists
platform_system_cfg_file = layout_path / f'system_{platform_name}_{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)}")
system_config_values = None
else:
system_config_values = common.get_config_file_values(str(platform_system_cfg_file), ['r_ShadersRemoteCompiler',
'r_ShadersAllowCompilation',
'r_AssetProcessorShaderCompiler',
'r_ShaderCompilerServer'])
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})")
# 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')
if not bootstrap_asset_type:
warning_count += _warn("'bootstrap.cfg' is missing specifications for asset type.")
elif bootstrap_asset_type != asset_type:
warning_count += _warn(f"The asset type specified in bootstrap.cfg ({bootstrap_asset_type}) does not match the asset type specified for this deployment({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.")
elif system_config_values is not None:
shaders_remote_compiler = system_config_values.get('r_ShadersRemoteCompiler') or '0'
asset_processor_shader_compiler = system_config_values.get('r_AssetProcessorShaderCompiler') or '0'
shader_compiler_server = system_config_values.get('r_ShaderCompilerServer') or LOCAL_HOST
shaders_allow_compilation = system_config_values.get('r_ShadersAllowCompilation')
def _validate_remote_shader_settings():
if shader_compiler_server == LOCAL_HOST:
if asset_processor_shader_compiler != '1':
return _warn(f"Connection to the remote shader compiler (r_ShaderCompilerServer) is not properly "
f"set in system_{platform_name_lower}_{asset_type}.cfg. If it is set to {LOCAL_HOST}, then "
f"r_AssetProcessorShaderCompiler must be set to 1.")
else:
if _validate_remote_ap(remote_ip, remote_connect, False) > 0:
return _warn(f"The system_{platform_name_lower}_{asset_type}.cfg file is configured to connect to the"
f" shader compiler server through the remote connection to the Asset Processor.")
return 0
# Validation steps based on the asset mode
if asset_mode == ASSET_MODE_PAK:
# 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':
has_shader_pak = True
pak_count += 1
if pak_count == 0:
warning_count += _warn("No pak files found for PAK mode deployment")
# Check if the shader paks are set
if has_shader_pak:
# 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 "
f"(r_ShadersRemoteCompiler) is still enabled "
f"for it in system_{platform_name_lower}_{asset_type}.cfg.")
else:
# Since we are not connecting to the shader compiler, also make sure bootstrap is not configured to
# connect to Asset Processor remotely
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 "
f"(r_ShadersAllowCompilation) is still enabled "
f"for it in system_{platform_name_lower}_{asset_type}.cfg.")
else:
warning_count += _validate_remote_shader_settings()
elif asset_mode == ASSET_MODE_VFS:
remote_file_system = bootstrap_values.get(f'{platform_name_lower}_remote_filesystem') or '0'
if not remote_file_system != '1':
warning_count += _warn("Remote file system is not configured in bootstrap.cfg for VFS mode.")
else:
warning_count += _validate_remote_ap(remote_ip, remote_connect, True)
else:
# If there are no shader paks, make sure that a connection to the shader compiler is set
warning_count += _validate_remote_shader_settings()
return warning_count
def copy_asset_files_to_layout(game_name, game_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.
"""
src_asset_contents = os.listdir(game_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
# 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
continue
if src_file.startswith('system_'):
# For system files (system_<platform>_<asset_platform>), only allow the ones that are marked for the
# current <platform>
if not src_file.startswith(allowed_system_config_prefix):
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_dst = os.path.join(layout_target, src_file)
if os.path.isdir(abs_src):
# Skip all source folders
continue
# The target file exists, check whats at the target
if os.path.isdir(abs_dst):
# The target destination is a folder, we will skip
logging.warning("Skipping layout copying of file '%s' because the target '%s' refers to a directory",
src_file,
abs_dst)
continue
if os.path.isfile(abs_dst):
# The target is a file, do a fingerprint check
# TODO: Evaluate if we want to just junction the files instead of doing a copy
src_hash = common.file_fingerprint(abs_src)
dst_hash = common.file_fingerprint(abs_dst)
if src_hash == dst_hash:
logging.debug("Skipping layout copy of '%s', fingerprints of source and destination matches (%s)",
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):
"""
Helper function to either remove a symlink, or remove a folder
"""
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]
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)
def create_link(src, tgt, 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
:param src: The name of the link to create
:param tgt: The target of the new link
:param copy: Perform a directory copy instead of a link
"""
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]
else:
link_type = 'soft link'
junction_cmd = ['ln', '-s', 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)
def construct_and_validate_cache_game_asset_folder(dev_root, game_name, asset_type, warn_on_missing_game_cache):
"""
Given the parameters for a game (name, dev root, 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
"""
# 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:
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),
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):
# TODO: Note, this is only checking the existence of the folder, not for any content validation
return cache_game_asset_folder
# Expected source of the game assets was not found
if not warn_on_missing_game_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),
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):
"""
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 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
:param override_pak_folder: The optional path to override the default pak folder for PAK asset mode (N/A for this function)
: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)
game_asset_folder = construct_and_validate_cache_game_asset_folder(dev_root=dev_root,
game_name=game,
asset_type=asset_type,
warn_on_missing_game_cache=warning_on_missing_assets)
game_folder = game.lower()
vfs_asset_source = os.path.join(game_asset_folder, game_folder, 'config')
if not os.path.isdir(vfs_asset_source):
raise common.LmbrCmdError("Cache folder for the game '{}' missing 'config' folder".format(game),
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'))
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_game_config_path = os.path.join(temp_vfs_layout_game_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)
# Create the link
create_link(vfs_asset_source, temp_vfs_layout_game_config_path, copy)
# Create the assets to the layout
copy_asset_files_to_layout(game_name=game,
game_asset_folder=game_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_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):
"""
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 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
:param override_pak_folder: The optional path to override the default pak folder for PAK asset mode (N/A for this function)
:param copy: Option to copy instead of attempting to symlink/junction
"""
assert mode in (ASSET_MODE_PAK, ASSET_MODE_LOOSE)
game_folder = game.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):
if warning_on_missing_assets:
logging.warning("Pak folder for the game '{}' is missing (expected at '{}'). Skipping layout sync".format(game, game_asset_folder))
return
else:
raise common.LmbrCmdError("Pak folder for the game '{}' is missing (expected at '{}')".format(game, game_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,
asset_type=asset_type,
warn_on_missing_game_cache=warning_on_missing_assets)
if not game_asset_folder:
logging.warning(
"Cannot locate built assets for game '{}' (expected at '{}'). Skipping layout sync".format(game,
game_asset_folder))
return
else:
assert False, "Invalid Mode {}".format(mode)
# Create the assets to the layout
copy_asset_files_to_layout(game_name=game,
game_asset_folder=game_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')
# 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,
override_pak_folder, copy):
sync_layout_non_vfs(mode=ASSET_MODE_PAK,
target_platform=target_platform,
dev_root=dev_root,
game=game,
asset_type=asset_type,
warning_on_missing_assets=warning_on_missing_assets,
layout_target=layout_target,
override_pak_folder=override_pak_folder,
copy=copy)
def sync_layout_loose(dev_root, target_platform, game, 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,
asset_type=asset_type,
warning_on_missing_assets=warning_on_missing_assets,
layout_target=layout_target,
override_pak_folder=override_pak_folder,
copy=copy)
ASSET_SYNC_MODE_FUNCTION = {
ASSET_MODE_VFS: sync_layout_vfs,
ASSET_MODE_PAK: sync_layout_pak,
ASSET_MODE_LOOSE: sync_layout_loose
}
def main(args):
parser = argparse.ArgumentParser(description="Synchronize a game's assets to a Xenia 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.',
required=True)
parser.add_argument('-p', '--platform',
help='Target platform for the layout.',
required=True)
parser.add_argument('-a', '--asset-type',
help='The asset type to use for this deployment',
default='pc')
parser.add_argument('--debug',
action='store_true',
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')
parser.add_argument('-m', '--mode',
type=str,
choices=ALL_ASSET_MODES,
default=ASSET_MODE_LOOSE,
help='Asset Mode (vfs|pak|loose)')
parser.add_argument('-l', '--layout-root',
help='The layout root to where the sync of the assets will occur',
required=True)
parser.add_argument('--create-layout-root',
action='store_true',
help='If the layout root doesnt exist, create it')
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')
parser.add_argument('--build-config',
default='',
help='(optional) If provided, will adjust the asset mode if the provided build-config is "release"')
parser.add_argument('-c', '--copy',
action='store_true',
help='Copy the files instead of symlinking.')
parser.add_argument('--verify',
action='store_true',
help='Option to perform a verification and report warnings against bootstrap and system configs based on the asset mode and type.')
parser.add_argument('--fail-on-warning',
action='store_true',
help='Option to perform a verification of the layout against the bootstrap and system configs.')
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),
common.ERROR_CODE_INVALID_PARAMETER)
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:
try:
os.makedirs(parsed_args.layout_root, exist_ok=True)
except OSError as e:
raise common.LmbrCmdError("Unable to create layout folder '{}': {}".format(e,
parsed_args.layout_root),
common.ERROR_CODE_ERROR_DIRECTORY)
else:
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,
asset_mode=input_asset_mode,
asset_type=parsed_args.asset_type)
if warnings > 0:
if parsed_args.fail_on_warning:
raise common.LmbrCmdError(f"Layout verification failed: {warnings} warnings.")
logging.warning("%d layout warnings", warnings)
if __name__ == '__main__':
try:
main(sys.argv[1:])
exit(0)
except common.LmbrCmdError as err:
print(str(err), file=sys.stderr)
exit(err.code)
+6
View File
@@ -0,0 +1,6 @@
[pytest]
python_files = unit_test*.py
addopts = --pyargs
testpaths = .
log_cli = true
python_functions = test_*
+259
View File
@@ -0,0 +1,259 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
import os
import pytest
from . import add_remove_gem
TEST_WITHOUT_NO_GEM_CONTENT = """
# {BEGIN_LICENSE}
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# {END_LICENSE}
set(GEM_DEPENDENCIES
)
"""
TEST_WITHOUT_ONLY_GEM_CONTENT = """
# {BEGIN_LICENSE}
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# {END_LICENSE}
set(GEM_DEPENDENCIES
Gem::TestGem
)
"""
TEST_WITHOUT_ADDED_GEM_CONTENT = """
# {BEGIN_LICENSE}
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# {END_LICENSE}
set(GEM_DEPENDENCIES
Gem::ExistingGem
)
"""
TEST_WITH_ADDED_GEM_CONTENT = """
# {BEGIN_LICENSE}
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# {END_LICENSE}
set(GEM_DEPENDENCIES
Gem::TestGem
Gem::ExistingGem
)
"""
@pytest.mark.parametrize(
"contents, gem, expected_result, runtime_present, expect_failure", [
pytest.param(TEST_WITHOUT_ADDED_GEM_CONTENT, "TestGem", TEST_WITH_ADDED_GEM_CONTENT, True, False),
pytest.param(TEST_WITHOUT_ADDED_GEM_CONTENT, "TestGem", TEST_WITH_ADDED_GEM_CONTENT, False, True),
pytest.param(TEST_WITHOUT_ADDED_GEM_CONTENT, "/TestGem", TEST_WITH_ADDED_GEM_CONTENT, True, True),
pytest.param(TEST_WITHOUT_NO_GEM_CONTENT, "TestGem", TEST_WITHOUT_ONLY_GEM_CONTENT, True, False),
]
)
def test_add_gem_dependency(tmpdir, contents, gem, expected_result, runtime_present, expect_failure):
dev_root = str(tmpdir.join('dev').realpath()).replace('\\', '/')
os.makedirs(dev_root, exist_ok=True)
dev_project_gem_code = f'{dev_root}/TestProject/Gem/Code'
os.makedirs(dev_project_gem_code, exist_ok=True)
runtime_dependencies_cmake_file = f'{dev_project_gem_code}/runtime_dependencies.cmake'
if runtime_present:
if os.path.isfile(runtime_dependencies_cmake_file):
os.unlink(runtime_dependencies_cmake_file)
with open(runtime_dependencies_cmake_file, 'a') as s:
s.write(contents)
result = add_remove_gem.add_gem_dependency(runtime_dependencies_cmake_file, gem)
if expect_failure:
assert result != 0
else:
assert result == 0
with open(runtime_dependencies_cmake_file, 'r') as s:
s_data = s.read()
assert s_data == expected_result
@pytest.mark.parametrize(
"contents, gem, expected_result, runtime_present, expect_failure", [
pytest.param(TEST_WITH_ADDED_GEM_CONTENT, "TestGem", TEST_WITHOUT_ADDED_GEM_CONTENT, True, False),
pytest.param(TEST_WITH_ADDED_GEM_CONTENT, "TestGem", TEST_WITHOUT_ADDED_GEM_CONTENT, False, True),
pytest.param(TEST_WITHOUT_ADDED_GEM_CONTENT, "TestGem", TEST_WITHOUT_ADDED_GEM_CONTENT, True, True)
]
)
def test_remove_gem_dependency(tmpdir, contents, gem, expected_result, runtime_present, expect_failure):
dev_root = str(tmpdir.join('dev').realpath()).replace('\\', '/')
os.makedirs(dev_root, exist_ok=True)
dev_project_gem_code = f'{dev_root}/TestProject/Gem/Code'
os.makedirs(dev_project_gem_code, exist_ok=True)
runtime_dependencies_cmake_file = f'{dev_project_gem_code}/runtime_dependencies.cmake'
if runtime_present:
if os.path.isfile(runtime_dependencies_cmake_file):
os.unlink(runtime_dependencies_cmake_file)
with open(runtime_dependencies_cmake_file, 'a') as s:
s.write(contents)
result = add_remove_gem.remove_gem_dependency(runtime_dependencies_cmake_file, gem)
if expect_failure:
assert result != 0
else:
assert result == 0
with open(runtime_dependencies_cmake_file, 'r') as s:
s_data = s.read()
assert s_data == expected_result
@pytest.mark.parametrize("add,"
" contents, gem, project, expected_result,"
" runtime_present, tool_present,"
" ask_for_runtime, ask_for_tool,"
" expect_failure", [
pytest.param(True,
TEST_WITHOUT_ADDED_GEM_CONTENT, "TestGem", "TestProject",
TEST_WITH_ADDED_GEM_CONTENT,
True, True,
True, True,
False),
pytest.param(True,
TEST_WITHOUT_ADDED_GEM_CONTENT, "TestGem", "TestProject",
TEST_WITH_ADDED_GEM_CONTENT,
True, False,
True, True,
True),
pytest.param(True,
TEST_WITHOUT_ADDED_GEM_CONTENT, "TestGem", "TestProject",
TEST_WITH_ADDED_GEM_CONTENT,
False, True,
True, True,
True),
pytest.param(True,
TEST_WITHOUT_ADDED_GEM_CONTENT, "TestGem", "TestProject",
TEST_WITH_ADDED_GEM_CONTENT,
False, False,
True, True,
True),
pytest.param(False,
TEST_WITH_ADDED_GEM_CONTENT, "TestGem", "TestProject",
TEST_WITHOUT_ADDED_GEM_CONTENT,
True, True,
True, True,
False),
pytest.param(False,
TEST_WITH_ADDED_GEM_CONTENT, "TestGem", "TestProject",
TEST_WITHOUT_ADDED_GEM_CONTENT,
True, False,
True, True,
True),
pytest.param(False,
TEST_WITH_ADDED_GEM_CONTENT, "TestGem", "TestProject",
TEST_WITHOUT_ADDED_GEM_CONTENT,
False, True,
True, True,
True),
pytest.param(False,
TEST_WITH_ADDED_GEM_CONTENT, "TestGem", "TestProject",
TEST_WITHOUT_ADDED_GEM_CONTENT,
False, False,
True, True,
True)
]
)
def test_add_remove_gem(tmpdir,
add,
contents, gem, project,
expected_result,
runtime_present, tool_present,
ask_for_runtime, ask_for_tool,
expect_failure):
dev_root = str(tmpdir.join('dev').realpath()).replace('\\', '/')
os.makedirs(dev_root, exist_ok=True)
dev_project_gem_code = f'{dev_root}/TestProject/Gem/Code'
os.makedirs(dev_project_gem_code, exist_ok=True)
runtime_dependencies_cmake_file = f'{dev_project_gem_code}/runtime_dependencies.cmake'
if runtime_present:
if os.path.isfile(runtime_dependencies_cmake_file):
os.unlink(runtime_dependencies_cmake_file)
with open(runtime_dependencies_cmake_file, 'a') as s:
s.write(contents)
tool_dependencies_cmake_file = f'{dev_project_gem_code}/tool_dependencies.cmake'
os.makedirs(dev_project_gem_code, exist_ok=True)
if tool_present:
if os.path.isfile(tool_dependencies_cmake_file):
os.unlink(tool_dependencies_cmake_file)
with open(tool_dependencies_cmake_file, 'w') as s:
s.write(contents)
project_folder = f'{dev_root}/TestProject'
os.makedirs(project_folder, exist_ok=True)
gems_folder = f'{dev_root}/Gems'
os.makedirs(gems_folder, exist_ok=True)
gem_folder = f'{gems_folder}/{gem}'
os.makedirs(gem_folder, exist_ok=True)
result = add_remove_gem.add_remove_gem(add, dev_root, gem, project, ask_for_runtime, ask_for_tool)
if expect_failure:
assert result != 0
else:
assert result == 0
if runtime_present:
with open(runtime_dependencies_cmake_file, 'r') as s:
s_data = s.read()
assert s_data == expected_result
if tool_present:
with open(tool_dependencies_cmake_file, 'r') as s:
s_data = s.read()
assert s_data == expected_result
+431
View File
@@ -0,0 +1,431 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
import configparser
import hashlib
import json
import os
import pathlib
import pytest
import re
from . import common
@pytest.mark.parametrize(
"engine_json_content, expected_success", [
pytest.param({'fake': 'foo'}, True, id="TestSuccess"),
pytest.param(None, False, id="TestFail")
])
def test_determine_dev_root(tmpdir, engine_json_content, expected_success):
test_folder_heirarchy = 'dev/foo1/foo2/foo3/'
tmpdir.ensure(test_folder_heirarchy)
if engine_json_content:
fake_engine_json_content = json.dumps(engine_json_content,
sort_keys=True,
separators=(',', ': '),
indent=4)
engine_json_file = tmpdir.join('dev/engine.json')
engine_json_file.write(fake_engine_json_content)
expected_path = str(tmpdir.join('dev/').realpath())
else:
expected_path = None
starting_path = str(tmpdir.join(test_folder_heirarchy).realpath())
result = common.determine_dev_root(starting_path)
if expected_path:
assert os.path.normcase(result) == os.path.normcase(expected_path)
else:
assert result is None
TEST_BOOTSTRAP_CONTENT_1 = """
sys_game_folder = Game1
foo = bar
key1 = value1
key2 = value2
assets = pc
--No Assets
"""
TEST_BOOTSTRAP_CONTENT_2 = """
sys_game_folder = Game2
foo = bar
#-------------------------
key1 = value1
key2 = value2
assets = pc
--No Assets
"""
@pytest.mark.parametrize(
"contents, input_keys, expected_result_map", [
pytest.param(TEST_BOOTSTRAP_CONTENT_1, ['sys_game_folder', 'foo', 'assets'], {'sys_game_folder': 'Game1',
'foo': 'bar',
'assets': 'pc'}, id="TestFullMatch"),
pytest.param(TEST_BOOTSTRAP_CONTENT_2, ['sys_game_folder', 'foo', 'barnone'], {'sys_game_folder': 'Game2',
'foo': 'bar'}, id="TestPartialMatch"),
pytest.param(TEST_BOOTSTRAP_CONTENT_2, ['sys_game_foldernone', 'foonone', 'barnone'], {}, id="TestNoMatch")
]
)
def test_get_bootstrap_values_success(tmpdir, contents, input_keys, expected_result_map):
test_dev_root = 'dev'
tmpdir.ensure('{}/bootstrap.cfg'.format(test_dev_root))
bootstrap_file = tmpdir.join('{}/bootstrap.cfg'.format(test_dev_root))
bootstrap_file.write(contents)
bootstrap_file_path = str(tmpdir.join(test_dev_root).realpath())
result = common.get_bootstrap_values(bootstrap_file_path, input_keys)
assert expected_result_map == result
def test_get_bootstrap_values_fail():
try:
bad_file = 'x:\\foo\\bar\\file\\'
common.get_bootstrap_values(bad_file, ['input_keys'])
except common.LmbrCmdError as err:
assert 'Missing' in str(err)
else:
assert False, "Excepted LayoutToolError (missing file)"
TEST_AP_CONFIG_1 = """
[Platforms]
;pc=enabled
;xenia=enabled
"""
TEST_AP_CONFIG_2 = """
[Platforms]
;pc=enabled
xenia=enabled
"""
TEST_AP_CONFIG_3 = """
[Platforms]
pc=disabled
xenia=enabled
"""
@pytest.mark.parametrize(
"contents, check_asset_type, expected_result", [
pytest.param(TEST_AP_CONFIG_1, 'xenia', False, id='XeniaDisabled'),
pytest.param(TEST_AP_CONFIG_2, 'xenia', True, id='XeniaEnabled'),
pytest.param(TEST_AP_CONFIG_3, 'xenia', True, id='XeniaEnabled'),
pytest.param(TEST_AP_CONFIG_1, 'pc', True, id='XeniaDisabled'),
pytest.param(TEST_AP_CONFIG_3, 'pc', False, id='XeniaDisabled'),
]
)
def test_validate_ap_config_asset_type_enabled(tmpdir, contents, check_asset_type, expected_result):
test_dev_root = 'dev'
tmpdir.ensure('{}/AssetProcessorPlatformConfig.ini'.format(test_dev_root))
ap_config_file = tmpdir.join('{}/AssetProcessorPlatformConfig.ini'.format(test_dev_root))
ap_config_file.write(contents)
ap_config_file_path = str(tmpdir.join(test_dev_root).realpath())
result = common.validate_ap_config_asset_type_enabled(ap_config_file_path, check_asset_type)
assert expected_result == result
@pytest.mark.parametrize(
"filename, file_mtime, file_size, contents, deep_check", [
pytest.param('alpha.txt', 1000, 1000, "Alpha Alpha Alpha", False, id="TestShallow"),
pytest.param('alpha.txt', 1000, 1000, "Alpha Alpha Alpha", True, id="TestDeepMatch"),
pytest.param('beta.txt', 1000, 1000, "Beta Beta Beta", False, id="TestShallowMatch2"),
pytest.param('beta.txt', 1001, 1000, "Beta Beta Beta", False, id="TestShallowMatch3"),
pytest.param('ceti.txt', 2000, 2000, "Ceti Ceti Ceti", True, id="TestDeepMatch3"),
]
)
def test_file_fingerprint_success(tmpdir, filename, file_mtime, file_size, contents, deep_check):
backup_stat = os.stat
tmpdir.ensure(filename)
ap_config_file = tmpdir.join(filename)
ap_config_file.write(contents)
full_path = str(tmpdir.join(filename).realpath())
try:
class MockStatResult(object):
def __init__(self):
self.st_mtime = file_mtime
self.st_size = file_size
def _mock_stat(path):
assert path == full_path
return MockStatResult()
os.stat = _mock_stat
expected_hasher = hashlib.md5()
expected_hasher.update(str(file_mtime).encode('UTF-8'))
expected_hasher.update(str(file_size).encode('UTF-8'))
# If doing a deep check, also include the contents
if deep_check:
expected_hasher.update(contents.encode('UTF-8'))
expected_result = expected_hasher.hexdigest()
result = common.file_fingerprint(full_path, deep_check)
assert result == expected_result
finally:
os.stat = backup_stat
def test_load_template_file_success(tmpdir):
tmpdir.ensure('test_template.txt.in')
test_template_content = """
### Copyright will be removed from template
[test]
subjectA = ${subject_A_value}
subjectB = ${subject_B_value}
"""
expected_a = 'foo'
expected_b = 'bar'
test_template_file = tmpdir / 'test_template.txt.in'
test_template_file.write_text(test_template_content, encoding='ascii')
test_template_env = {
'subject_A_value': expected_a,
'subject_B_value': expected_b
}
result = common.load_template_file(pathlib.Path(str(test_template_file.realpath())), test_template_env)
assert '###' not in result
validator = configparser.ConfigParser()
validator.read_string(result)
validate_subjA = validator.get('test', 'subjectA')
validate_subjB = validator.get('test', 'subjectB')
assert validate_subjA == expected_a
assert validate_subjB == expected_b
TEST_GAME_PROJECT_JSON_FORMAT = """
{{
"project_name": "{game_name}",
"product_name": "{game_name}",
"executable_name": "{game_name}.GameLauncher",
"modules" : [],
"project_id": "{{4F3363D3-4A7C-47A6-B464-B21524771358}}",
"android_settings" : {{
"package_name" : "com.lumberyard.yourgame",
"version_number" : 1,
"version_name" : "1.0.0.0",
"orientation" : "landscape"
}},
"xenia_settings" : {{
}},
"provo_settings": {{
}}
}}
"""
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)
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'
project_json_path.write_text(game_project_json, encoding='ascii')
result_game_name, _ = common.verify_game_project_and_dev_root(game_folder,
str(tmpdir.join(dev_root).realpath()))
assert result_game_name == game_name
def test_platform_last_settings_success(tmpdir):
tmpdir.ensure('platform.list')
test_build_dir = "c:/test/path"
test_projects_str = "ProjA;ProjB"
test_projects = test_projects_str.split(';')
test_asset_deploy_mode = "LOOSE"
test_asset_deploy_type = "pc"
test_platform = 'foo'
last_settings_content = f"""
# Auto Generated from last cmake project generation (2020-07-24T12:10:47)
[settings]
platform={test_platform}
game_projects={test_projects_str}
asset_deploy_mode={test_asset_deploy_mode}
asset_deploy_type={test_asset_deploy_type}
"""
test_last_file = tmpdir / 'platform.settings'
test_last_file.write_text(last_settings_content, encoding='ascii')
result = common.PlatformSettings(tmpdir.realpath())
assert result.projects == test_projects
assert result.asset_deploy_mode == test_asset_deploy_mode
assert result.asset_deploy_type == test_asset_deploy_type
def test_transform_bootstrap_sysgamefolder(tmpdir):
tmpdir.ensure('bootstrap.cfg')
test_bootstrap_content = """
-- Blah Blah
-- Blah Blah
sys_game_folder=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_folder\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):
tmpdir.ensure('bootstrap.cfg')
test_bootstrap_content = """
-- Blah Blah
-- Blah Blah
-- 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_folder\s*=\s*(.*)", transformed_text)
assert search_gamename
assert search_gamename.group(1)
assert search_gamename.group(1) == test_game_name
def test_cmake_dependency_success(tmpdir):
test_module = 'FooBar'
tmpdir.ensure(f'Registry/cmake_dependencies.{test_module.lower()}.setreg')
test_setreg_path = tmpdir / 'Registry' / f'cmake_dependencies.{test_module.lower()}.setreg'
test_module_1 = "Gem.Maestro.Editor.3b9a978ed6f742a1acb99f74379a342c.v0.1.0.dll"
test_module_2 = "Gem.TextureAtlas.5a149b6b3c964064bd4970f0e92f72e2.v0.1.0.dll"
test_retreg_content = f"""
{{
"Amazon":
{{
"Gems":
{{
"Maestro.Editor":
{{
"Module":"{test_module_1}",
"SourcePaths":["Gems/Maestro"]
}},
"TextureAtlas":
{{
"Module":"{test_module_2}",
"SourcePaths":["Gems/TextureAtlas"]
}}
}}
}}
}}
"""
test_setreg_path.write_text(test_retreg_content, encoding='ascii')
result = common.get_cmake_dependency_modules(tmpdir, test_module, 'Gems')
assert result
assert test_module_1 in result
assert test_module_2 in result
+102
View File
@@ -0,0 +1,102 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
import os
import pytest
from . import current_project
TEST_BOOTSTRAP_CONTENT_1 = """
sys_game_folder = Game1
foo = bar
key1 = value1
key2 = value2
assets = pc
"""
TEST_BOOTSTRAP_CONTENT_2 = """
sys_game_folder=Game1
foo = bar
key1 = value1
key2 = value2
assets = pc
"""
TEST_BOOTSTRAP_CONTENT_3 = """
sys_game_folder= Game1
foo = bar
key1 = value1
key2 = value2
assets = pc
"""
TEST_BOOTSTRAP_CONTENT_4 = """
sys_game_folder =Game1
foo = bar
key1 = value1
key2 = value2
assets = pc
"""
TEST_BOOTSTRAP_CONTENT_5 = """
sys_game_folder = Game1
foo = bar
key1 = value1
key2 = value2
assets = pc
"""
@pytest.mark.parametrize(
"contents, expected_result", [
pytest.param(TEST_BOOTSTRAP_CONTENT_1, 'Game1'),
pytest.param(TEST_BOOTSTRAP_CONTENT_2, 'Game1'),
pytest.param(TEST_BOOTSTRAP_CONTENT_3, 'Game1'),
pytest.param(TEST_BOOTSTRAP_CONTENT_4, 'Game1'),
pytest.param(TEST_BOOTSTRAP_CONTENT_5, 'Game1'),
]
)
def test_get_current_project(tmpdir, contents, expected_result):
dev_root = str(tmpdir.join('dev').realpath()).replace('\\', '/')
os.makedirs(dev_root, exist_ok=True)
bootstrap_file = f'{dev_root}/bootstrap.cfg'
if os.path.isfile(bootstrap_file):
os.unlink(bootstrap_file)
with open(bootstrap_file, 'a') as s:
s.write(contents)
result = current_project.get_current_project(dev_root)
assert expected_result == result
@pytest.mark.parametrize(
"contents, project_to_set, expected_result", [
pytest.param(TEST_BOOTSTRAP_CONTENT_1, 'Test1', 0),
pytest.param(TEST_BOOTSTRAP_CONTENT_1, ' Test2', 0),
pytest.param(TEST_BOOTSTRAP_CONTENT_1, 'Test3 ', 0),
pytest.param(TEST_BOOTSTRAP_CONTENT_1, '/Test4', 1),
pytest.param(TEST_BOOTSTRAP_CONTENT_1, '=Test5', 1),
]
)
def test_set_current_project(tmpdir, contents, project_to_set, expected_result):
dev_root = str(tmpdir.join('dev').realpath()).replace('\\', '/')
os.makedirs(dev_root, exist_ok=True)
bootstrap_file = f'{dev_root}/bootstrap.cfg'
if os.path.isfile(bootstrap_file):
os.unlink(bootstrap_file)
with open(bootstrap_file, 'a') as s:
s.write(contents)
result = current_project.set_current_project(dev_root, project_to_set)
assert expected_result == result
if result == 0:
project_that_is_set = current_project.get_current_project(dev_root)
print(project_that_is_set)
print(project_to_set)
assert project_to_set.strip() == project_that_is_set
+748
View File
@@ -0,0 +1,748 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
import os
import pytest
from . import engine_template
TEST_TEMPLATED_CONTENT_WITH_LICENSE = """\
// {BEGIN_LICENSE}
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
// {END_LICENSE}
#pragma once
#include <AzCore/EBus/EBus.h>
namespace ${Name}
{
class ${Name}Requests
: public AZ::EBusTraits
{
public:
//////////////////////////////////////////////////////////////////////////
// EBusTraits overrides
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
//////////////////////////////////////////////////////////////////////////
// Put your public methods here
};
using ${Name}RequestsBus = AZ::EBus<${Name}Requests>;
} // namespace ${Name}
"""
TEST_TEMPLATED_CONTENT_WITHOUT_LICENSE = """\
#pragma once
#include <AzCore/EBus/EBus.h>
namespace ${Name}
{
class ${Name}Requests
: public AZ::EBusTraits
{
public:
//////////////////////////////////////////////////////////////////////////
// EBusTraits overrides
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
//////////////////////////////////////////////////////////////////////////
// Put your public methods here
};
using ${Name}RequestsBus = AZ::EBus<${Name}Requests>;
} // namespace ${Name}
"""
TEST_CONCRETE_TESTTEMPLATE_CONTENT_WITHOUT_LICENSE = """\
#pragma once
#include <AzCore/EBus/EBus.h>
namespace TestTemplate
{
class TestTemplateRequests
: public AZ::EBusTraits
{
public:
//////////////////////////////////////////////////////////////////////////
// EBusTraits overrides
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
//////////////////////////////////////////////////////////////////////////
// Put your public methods here
};
using TestTemplateRequestsBus = AZ::EBus<TestTemplateRequests>;
} // namespace TestTemplate
"""
TEST_CONCRETE_TESTTEMPLATE_CONTENT_WITH_LICENSE = """\
// {BEGIN_LICENSE}
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
// {END_LICENSE}
#pragma once
#include <AzCore/EBus/EBus.h>
namespace TestTemplate
{
class TestTemplateRequests
: public AZ::EBusTraits
{
public:
//////////////////////////////////////////////////////////////////////////
// EBusTraits overrides
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
//////////////////////////////////////////////////////////////////////////
// Put your public methods here
};
using TestTemplateRequestsBus = AZ::EBus<TestTemplateRequests>;
} // namespace TestTemplate
"""
TEST_CONCRETE_TESTPROJECT_TEMPLATE_CONTENT_WITHOUT_LICENSE = """\
#pragma once
#include <AzCore/EBus/EBus.h>
namespace TestProject
{
class TestProjectRequests
: public AZ::EBusTraits
{
public:
//////////////////////////////////////////////////////////////////////////
// EBusTraits overrides
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
//////////////////////////////////////////////////////////////////////////
// Put your public methods here
};
using TestProjectRequestsBus = AZ::EBus<TestProjectRequests>;
} // namespace TestProject
"""
TEST_CONCRETE_TESTPROJECT_TEMPLATE_CONTENT_WITH_LICENSE = """\
// {BEGIN_LICENSE}
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
// {END_LICENSE}
#pragma once
#include <AzCore/EBus/EBus.h>
namespace TestProject
{
class TestProjectRequests
: public AZ::EBusTraits
{
public:
//////////////////////////////////////////////////////////////////////////
// EBusTraits overrides
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
//////////////////////////////////////////////////////////////////////////
// Put your public methods here
};
using TestProjectRequestsBus = AZ::EBus<TestProjectRequests>;
} // namespace TestProject
"""
TEST_CONCRETE_TESTGEM_TEMPLATE_CONTENT_WITHOUT_LICENSE = """\
#pragma once
#include <AzCore/EBus/EBus.h>
namespace TestGem
{
class TestGemRequests
: public AZ::EBusTraits
{
public:
//////////////////////////////////////////////////////////////////////////
// EBusTraits overrides
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
//////////////////////////////////////////////////////////////////////////
// Put your public methods here
};
using TestGemRequestsBus = AZ::EBus<TestGemRequests>;
} // namespace TestGem
"""
TEST_CONCRETE_TESTGEM_TEMPLATE_CONTENT_WITH_LICENSE = """\
// {BEGIN_LICENSE}
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
// {END_LICENSE}
#pragma once
#include <AzCore/EBus/EBus.h>
namespace TestGem
{
class TestGemRequests
: public AZ::EBusTraits
{
public:
//////////////////////////////////////////////////////////////////////////
// EBusTraits overrides
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
//////////////////////////////////////////////////////////////////////////
// Put your public methods here
};
using TestGemRequestsBus = AZ::EBus<TestGemRequests>;
} // namespace TestGem
"""
TEST_DEFAULTTEMPLATE_JSON_CONTENTS = """\
{
"inputPath": "Templates/Default/Template",
"copyFiles": [
{
"inFile": "Code/Include/${Name}/${Name}Bus.h",
"outFile": "Code/Include/${Name}/${Name}Bus.h",
"isTemplated": true,
"isOptional": false
}
],
"createDirectories": [
{
"outDir": "Code"
},
{
"outDir": "Code/Include"
},
{
"outDir": "Code/Include/Platform"
},
{
"outDir": "Code/Include/${Name}"
}
]
}\
"""
TEST_DEFAULTTEMPLATE_RESTRICTED_JSON_CONTENTS = """\
{
"inputPath": "restricted/Salem/Templates/Default/Template",
"copyFiles": [
{
"inFile": "Code/Include/Platform/Salem/${Name}Bus.h",
"outFile": "Code/Include/Platform/Salem/${Name}Bus.h",
"isTemplated": true,
"isOptional": false
}
],
"createDirectories": [
{
"outDir": "Code/Include/Platform/Salem"
}
]
}\
"""
TEST_DEFAULTPROJECT_TEMPLATE_JSON_CONTENTS = """\
{
"inputPath": "Templates/DefaultProject/Template",
"copyFiles": [
{
"inFile": "Code/Include/${Name}/${Name}Bus.h",
"outFile": "Code/Include/${Name}/${Name}Bus.h",
"isTemplated": true,
"isOptional": false
}
],
"createDirectories": [
{
"outDir": "Code"
},
{
"outDir": "Code/Include"
},
{
"outDir": "Code/Include/Platform"
},
{
"outDir": "Code/Include/${Name}"
}
]
}\
"""
TEST_DEFAULTPROJECT_TEMPLATE_RESTRICTED_JSON_CONTENTS = """\
{
"inputPath": "restricted/Salem/Templates/DefaultProject/Template",
"copyFiles": [
{
"inFile": "Code/Include/Platform/Salem/${Name}Bus.h",
"outFile": "Code/Include/Platform/Salem/${Name}Bus.h",
"isTemplated": true,
"isOptional": false
}
],
"createDirectories": [
{
"outDir": "Code/Include/Platform/Salem"
}
]
}\
"""
TEST_DEFAULTGEM_TEMPLATE_JSON_CONTENTS = """\
{
"inputPath": "Templates/DefaultGem/Template",
"copyFiles": [
{
"inFile": "Code/Include/${Name}/${Name}Bus.h",
"outFile": "Code/Include/${Name}/${Name}Bus.h",
"isTemplated": true,
"isOptional": false
}
],
"createDirectories": [
{
"outDir": "Code"
},
{
"outDir": "Code/Include"
},
{
"outDir": "Code/Include/Platform"
},
{
"outDir": "Code/Include/${Name}"
}
]
}\
"""
TEST_DEFAULTGEM_TEMPLATE_RESTRICTED_JSON_CONTENTS = """\
{
"inputPath": "restricted/Salem/Templates/DefaultGem/Template",
"copyFiles": [
{
"inFile": "Code/Include/Platform/Salem/${Name}Bus.h",
"outFile": "Code/Include/Platform/Salem/${Name}Bus.h",
"isTemplated": true,
"isOptional": false
}
],
"createDirectories": [
{
"outDir": "Code/Include/Platform/Salem"
}
]
}\
"""
@pytest.mark.parametrize(
"concrete_contents,"
" templated_contents_with_license, templated_contents_without_license,"
" keep_license_text, expect_failure,"
" template_json_contents, restricted_template_json_contents", [
pytest.param(TEST_CONCRETE_TESTTEMPLATE_CONTENT_WITH_LICENSE,
TEST_TEMPLATED_CONTENT_WITH_LICENSE, TEST_TEMPLATED_CONTENT_WITHOUT_LICENSE,
True, False,
TEST_DEFAULTTEMPLATE_JSON_CONTENTS, TEST_DEFAULTTEMPLATE_RESTRICTED_JSON_CONTENTS),
pytest.param(TEST_CONCRETE_TESTTEMPLATE_CONTENT_WITH_LICENSE,
TEST_TEMPLATED_CONTENT_WITH_LICENSE, TEST_TEMPLATED_CONTENT_WITHOUT_LICENSE,
False, False,
TEST_DEFAULTTEMPLATE_JSON_CONTENTS, TEST_DEFAULTTEMPLATE_RESTRICTED_JSON_CONTENTS)
]
)
def test_create_template(tmpdir,
concrete_contents,
templated_contents_with_license, templated_contents_without_license,
keep_license_text, expect_failure,
template_json_contents, restricted_template_json_contents):
dev_root = str(tmpdir.join('dev').realpath()).replace('\\', '/')
os.makedirs(dev_root, exist_ok=True)
dev_gem_code_include_testgem = f'{dev_root}/TestTemplate/Code/Include/TestTemplate'
os.makedirs(dev_gem_code_include_testgem, exist_ok=True)
gem_bus_file = f'{dev_gem_code_include_testgem}/TestTemplateBus.h'
if os.path.isfile(gem_bus_file):
os.unlink(gem_bus_file)
with open(gem_bus_file, 'w') as s:
s.write(concrete_contents)
dev_gem_code_include_platform_salem = f'{dev_root}/TestTemplate/Code/Include/Platform/Salem'
os.makedirs(dev_gem_code_include_platform_salem, exist_ok=True)
restricted_gem_bus_file = f'{dev_gem_code_include_platform_salem}/TestTemplateBus.h'
if os.path.isfile(restricted_gem_bus_file):
os.unlink(restricted_gem_bus_file)
with open(restricted_gem_bus_file, 'w') as s:
s.write(concrete_contents)
template_folder = f'{dev_root}/Templates'
os.makedirs(template_folder, exist_ok=True)
restricted_folder = f'{dev_root}/restricted'
os.makedirs(restricted_folder, exist_ok=True)
result = engine_template.create_template(dev_root, 'TestTemplate', 'Default', keep_license_text=keep_license_text)
if expect_failure:
assert result != 0
else:
assert result == 0
new_template_folder = f'{template_folder}/Default'
assert os.path.isdir(new_template_folder)
new_template_json = f'{new_template_folder}/Template.json'
assert os.path.isfile(new_template_json)
with open(new_template_json, 'r') as s:
s_data = s.read()
assert s_data == template_json_contents
new_default_name_bus_file = f'{new_template_folder}/Template/Code/Include/' + '${Name}/${Name}Bus.h'
assert os.path.isfile(new_default_name_bus_file)
with open(new_default_name_bus_file, 'r') as s:
s_data = s.read()
if keep_license_text:
assert s_data == templated_contents_with_license
else:
assert s_data == templated_contents_without_license
restricted_template_folder = f'{dev_root}/restricted/Salem/Templates'
new_restricted_template_folder = f'{restricted_template_folder}/Default'
assert os.path.isdir(new_restricted_template_folder)
new_restricted_template_json = f'{new_restricted_template_folder}/Template.json'
assert os.path.isfile(new_restricted_template_json)
with open(new_restricted_template_json, 'r') as s:
s_data = s.read()
assert s_data == restricted_template_json_contents
new_restricted_default_name_bus_file = f'{restricted_template_folder}' \
f'/Default/Template/Code/Include/Platform/Salem/' + '${Name}Bus.h'
assert os.path.isfile(new_restricted_default_name_bus_file)
with open(new_restricted_default_name_bus_file, 'r') as s:
s_data = s.read()
if keep_license_text:
assert s_data == templated_contents_with_license
else:
assert s_data == templated_contents_without_license
@pytest.mark.parametrize(
"concrete_contents, templated_contents,"
" keep_license_text, expect_failure,"
" template_json_contents, restricted_template_json_contents", [
pytest.param(TEST_CONCRETE_TESTTEMPLATE_CONTENT_WITH_LICENSE, TEST_TEMPLATED_CONTENT_WITH_LICENSE,
True, False,
TEST_DEFAULTTEMPLATE_JSON_CONTENTS, TEST_DEFAULTTEMPLATE_RESTRICTED_JSON_CONTENTS),
pytest.param(TEST_CONCRETE_TESTTEMPLATE_CONTENT_WITHOUT_LICENSE, TEST_TEMPLATED_CONTENT_WITH_LICENSE,
False, False,
TEST_DEFAULTTEMPLATE_JSON_CONTENTS, TEST_DEFAULTTEMPLATE_RESTRICTED_JSON_CONTENTS)
]
)
def test_create_from_template(tmpdir,
concrete_contents, templated_contents,
keep_license_text, expect_failure,
template_json_contents, restricted_template_json_contents):
dev_root = str(tmpdir.join('dev').realpath()).replace('\\', '/')
os.makedirs(dev_root, exist_ok=True)
template_default_folder = f'{dev_root}/Templates/Default'
os.makedirs(template_default_folder, exist_ok=True)
template_json = f'{template_default_folder}/Template.json'
if os.path.isfile(template_json):
os.unlink(template_json)
with open(template_json, 'w') as s:
s.write(template_json_contents)
default_name_bus_dir = f'{template_default_folder}/Template/Code/Include/' + '${Name}'
os.makedirs(default_name_bus_dir, exist_ok=True)
default_name_bus_file = f'{default_name_bus_dir}/' + '${Name}Bus.h'
if os.path.isfile(default_name_bus_file):
os.unlink(default_name_bus_file)
with open(default_name_bus_file, 'w') as s:
s.write(templated_contents)
restricted_template_default_folder = f'{dev_root}/restricted/Salem/Templates/Default'
os.makedirs(restricted_template_default_folder, exist_ok=True)
restricted_template_json = f'{restricted_template_default_folder}/Template.json'
if os.path.isfile(restricted_template_json):
os.unlink(restricted_template_json)
with open(restricted_template_json, 'w') as s:
s.write(restricted_template_json_contents)
restricted_default_name_bus_dir = f'{restricted_template_default_folder}/Template/Code/Include/Platform/Salem'
os.makedirs(restricted_default_name_bus_dir, exist_ok=True)
restricted_default_name_bus_file = f'{restricted_default_name_bus_dir}/' + '${Name}Bus.h'
if os.path.isfile(restricted_default_name_bus_file):
os.unlink(restricted_default_name_bus_file)
with open(restricted_default_name_bus_file, 'w') as s:
s.write(templated_contents)
result = engine_template.create_from_template(dev_root, 'TestTemplate', 'Default',
keep_license_text=keep_license_text)
if expect_failure:
assert result != 0
else:
assert result == 0
test_folder = f'{dev_root}/TestTemplate'
assert os.path.isdir(test_folder)
test_bus_file = f'{test_folder}/Code/Include/TestTemplate/TestTemplateBus.h'
assert os.path.isfile(test_bus_file)
with open(test_bus_file, 'r') as s:
s_data = s.read()
assert s_data == concrete_contents
restricted_test_bus_folder = f'{dev_root}/restricted/Salem/TestTemplate/Code/Include/Platform/Salem'
assert os.path.isdir(restricted_test_bus_folder)
restricted_default_name_bus_file = f'{restricted_test_bus_folder}/TestTemplateBus.h'
assert os.path.isfile(restricted_default_name_bus_file)
with open(restricted_default_name_bus_file, 'r') as s:
s_data = s.read()
assert s_data == concrete_contents
@pytest.mark.parametrize(
"concrete_contents, templated_contents,"
" keep_license_text, expect_failure,"
" template_json_contents, restricted_template_json_contents", [
pytest.param(TEST_CONCRETE_TESTPROJECT_TEMPLATE_CONTENT_WITH_LICENSE, TEST_TEMPLATED_CONTENT_WITH_LICENSE,
True, False,
TEST_DEFAULTPROJECT_TEMPLATE_JSON_CONTENTS, TEST_DEFAULTPROJECT_TEMPLATE_RESTRICTED_JSON_CONTENTS),
pytest.param(TEST_CONCRETE_TESTPROJECT_TEMPLATE_CONTENT_WITHOUT_LICENSE, TEST_TEMPLATED_CONTENT_WITH_LICENSE,
False, False,
TEST_DEFAULTPROJECT_TEMPLATE_JSON_CONTENTS, TEST_DEFAULTPROJECT_TEMPLATE_RESTRICTED_JSON_CONTENTS)
]
)
def test_create_project(tmpdir,
concrete_contents, templated_contents,
keep_license_text, expect_failure,
template_json_contents, restricted_template_json_contents):
dev_root = str(tmpdir.join('dev').realpath()).replace('\\', '/')
os.makedirs(dev_root, exist_ok=True)
template_default_folder = f'{dev_root}/Templates/DefaultProject'
os.makedirs(template_default_folder, exist_ok=True)
template_json = f'{template_default_folder}/Template.json'
if os.path.isfile(template_json):
os.unlink(template_json)
with open(template_json, 'w') as s:
s.write(template_json_contents)
default_name_bus_dir = f'{template_default_folder}/Template/Code/Include/' + '${Name}'
os.makedirs(default_name_bus_dir, exist_ok=True)
default_name_bus_file = f'{default_name_bus_dir}/' + '${Name}Bus.h'
if os.path.isfile(default_name_bus_file):
os.unlink(default_name_bus_file)
with open(default_name_bus_file, 'w') as s:
s.write(templated_contents)
restricted_template_default_folder = f'{dev_root}/restricted/Salem/Templates/DefaultProject'
os.makedirs(restricted_template_default_folder, exist_ok=True)
restricted_template_json = f'{restricted_template_default_folder}/Template.json'
if os.path.isfile(restricted_template_json):
os.unlink(restricted_template_json)
with open(restricted_template_json, 'w') as s:
s.write(restricted_template_json_contents)
restricted_default_name_bus_dir = f'{restricted_template_default_folder}/Template/Code/Include/Platform/Salem'
os.makedirs(restricted_default_name_bus_dir, exist_ok=True)
restricted_default_name_bus_file = f'{restricted_default_name_bus_dir}/' + '${Name}Bus.h'
if os.path.isfile(restricted_default_name_bus_file):
os.unlink(restricted_default_name_bus_file)
with open(restricted_default_name_bus_file, 'w') as s:
s.write(templated_contents)
result = engine_template.create_project(dev_root, 'TestProject', keep_license_text=keep_license_text)
if expect_failure:
assert result != 0
else:
assert result == 0
test_project_folder = f'{dev_root}/TestProject'
assert os.path.isdir(test_project_folder)
test_project_bus_file = f'{test_project_folder}/Code/Include/TestProject/TestProjectBus.h'
assert os.path.isfile(test_project_bus_file)
with open(test_project_bus_file, 'r') as s:
s_data = s.read()
assert s_data == concrete_contents
restricted_test_project_bus_folder = f'{dev_root}/restricted/Salem/TestProject/Code/Include/Platform/Salem'
assert os.path.isdir(restricted_test_project_bus_folder)
restricted_default_name_bus_file = f'{restricted_test_project_bus_folder}/TestProjectBus.h'
assert os.path.isfile(restricted_default_name_bus_file)
with open(restricted_default_name_bus_file, 'r') as s:
s_data = s.read()
assert s_data == concrete_contents
@pytest.mark.parametrize(
"concrete_contents, templated_contents,"
" keep_license_text, expect_failure,"
" template_json_contents, restricted_template_json_contents", [
pytest.param(TEST_CONCRETE_TESTGEM_TEMPLATE_CONTENT_WITH_LICENSE, TEST_TEMPLATED_CONTENT_WITH_LICENSE,
True, False,
TEST_DEFAULTGEM_TEMPLATE_JSON_CONTENTS, TEST_DEFAULTGEM_TEMPLATE_RESTRICTED_JSON_CONTENTS),
pytest.param(TEST_CONCRETE_TESTGEM_TEMPLATE_CONTENT_WITHOUT_LICENSE, TEST_TEMPLATED_CONTENT_WITH_LICENSE,
False, False,
TEST_DEFAULTGEM_TEMPLATE_JSON_CONTENTS, TEST_DEFAULTGEM_TEMPLATE_RESTRICTED_JSON_CONTENTS)
]
)
def test_create_gem(tmpdir,
concrete_contents, templated_contents,
keep_license_text, expect_failure,
template_json_contents, restricted_template_json_contents):
dev_root = str(tmpdir.join('dev').realpath()).replace('\\', '/')
os.makedirs(dev_root, exist_ok=True)
template_default_folder = f'{dev_root}/Templates/DefaultGem'
os.makedirs(template_default_folder, exist_ok=True)
template_json = f'{template_default_folder}/Template.json'
if os.path.isfile(template_json):
os.unlink(template_json)
with open(template_json, 'w') as s:
s.write(template_json_contents)
default_name_bus_dir = f'{template_default_folder}/Template/Code/Include/' + '${Name}'
os.makedirs(default_name_bus_dir, exist_ok=True)
default_name_bus_file = f'{default_name_bus_dir}/' + '${Name}Bus.h'
if os.path.isfile(default_name_bus_file):
os.unlink(default_name_bus_file)
with open(default_name_bus_file, 'w') as s:
s.write(templated_contents)
restricted_template_default_folder = f'{dev_root}/restricted/Salem/Templates/DefaultGem'
os.makedirs(restricted_template_default_folder, exist_ok=True)
restricted_template_json = f'{restricted_template_default_folder}/Template.json'
if os.path.isfile(restricted_template_json):
os.unlink(restricted_template_json)
with open(restricted_template_json, 'w') as s:
s.write(restricted_template_json_contents)
restricted_default_name_bus_dir = f'{restricted_template_default_folder}/Template/Code/Include/Platform/Salem'
os.makedirs(restricted_default_name_bus_dir, exist_ok=True)
restricted_default_name_bus_file = f'{restricted_default_name_bus_dir}/' + '${Name}Bus.h'
if os.path.isfile(restricted_default_name_bus_file):
os.unlink(restricted_default_name_bus_file)
with open(restricted_default_name_bus_file, 'w') as s:
s.write(templated_contents)
result = engine_template.create_gem(dev_root, 'TestGem', keep_license_text=keep_license_text)
if expect_failure:
assert result != 0
else:
assert result == 0
test_gem_folder = f'{dev_root}/Gems/TestGem'
assert os.path.isdir(test_gem_folder)
test_gem_bus_file = f'{test_gem_folder}/Code/Include/TestGem/TestGemBus.h'
assert os.path.isfile(test_gem_bus_file)
with open(test_gem_bus_file, 'r') as s:
s_data = s.read()
assert s_data == concrete_contents
restricted_test_gem_bus_folder = f'{dev_root}/restricted/Salem/Gems/TestGem/Code/Include/Platform/Salem'
assert os.path.isdir(restricted_test_gem_bus_folder)
restricted_default_name_bus_file = f'{restricted_test_gem_bus_folder}/TestGemBus.h'
assert os.path.isfile(restricted_default_name_bus_file)
with open(restricted_default_name_bus_file, 'r') as s:
s_data = s.read()
assert s_data == concrete_contents
+500
View File
@@ -0,0 +1,500 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
import hashlib
import os
import pytest
import shutil
import subprocess
import sys
import tempfile
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)
from cmake.Tools import common, layout_tool
def test_copy_asset_files_to_layout_success():
# Mock functions and preserve the originals to restore
old_os_listdir = os.listdir
old_os_path_isdir = os.path.isdir
old_os_path_isfile = os.path.isfile
old_common_filefingerprint = common.file_fingerprint
old_shutil_copy2 = shutil.copy2
try:
# Setup test vectors
# Blacklisted files, should not show up in the result
test_blacklisted_file = [
'assetprocessorplatformconfig.ini'
]
# System files that are not the same platform, so should skip
test_skip_system_files = [
'system_badplatform_pc',
'system_badplatform_badplatform'
]
# Source 'folders' will be skipped
test_skip_source_folders = [
'src_folder'
]
# Destination 'folders' will be skipped
test_skip_dest_is_folder = [
'fake_dst_folder'
]
# Skip files that are the same in the destination
test_dest_same_as_src = [
'dst_same_as_src'
]
# COPY files that are the same in the destination
test_dest_diff_as_src = [
'dst_diff_as_src'
]
# COPY files that are not in the destination
test_src_not_in_dst = [
'system_goodplatform_pc',
'good_src_1',
'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 + \
test_skip_system_files + \
test_skip_source_folders + \
test_skip_dest_is_folder + \
test_dest_same_as_src + \
test_dest_diff_as_src + \
test_src_not_in_dst
return mock_files
os.listdir = _mock_os_listdir
def _mock_os_path_isdir(path):
basename = os.path.basename(path)
if basename in test_skip_source_folders:
return True
if basename in test_skip_dest_is_folder:
return True
return False
os.path.isdir = _mock_os_path_isdir
def _mock_os_path_isfile(path):
basename = os.path.basename(path)
if basename in test_dest_same_as_src:
return True
if basename in test_dest_diff_as_src:
return True
return False
os.path.isfile = _mock_os_path_isfile
def _mock_common_file_fingerprint(path):
basename = os.path.basename(path)
dirname = os.path.dirname(path)
if basename in test_dest_same_as_src:
return "SOURCE_FINGERPRINT"
elif basename in test_dest_diff_as_src:
if dirname == test_game_asset_folder:
return "SOURCE_FINGERPRINT"
else:
return "TARGET_FINGERPRINT"
else:
assert False
common.file_fingerprint = _mock_common_file_fingerprint
result_copy_files = []
def _mock_shutil_copy2(src, dst):
assert os.path.basename(src) == os.path.basename(dst)
basename = os.path.basename(dst)
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,
target_platform=test_platform,
layout_target=test_layout_target)
assert len(test_expected_copied_files) == len(result_copy_files)
for expected_copied_file in test_expected_copied_files:
assert expected_copied_file in result_copy_files
finally:
os.listdir = old_os_listdir
os.path.isdir = old_os_path_isdir
os.path.isfile = old_os_path_isfile
common.file_fingerprint = old_common_filefingerprint
shutil.copy2 = old_shutil_copy2
def test_create_link_windows_success():
old_platform = layout_tool.PLATFORM_NAME
old_subprocess_check_call = subprocess.check_call
try:
layout_tool.PLATFORM_NAME = 'Windows'
src = "test_src"
dst = "test_dst"
expected = ['cmd', '/c', 'mklink', '/J', dst, src]
def _mock_subprocess_check_call(args, stdout=None, stderr=None):
assert args == expected
subprocess.check_call = _mock_subprocess_check_call
layout_tool.create_link(src, dst, False)
finally:
layout_tool.PLATFORM_NAME = old_platform
subprocess.check_call = old_subprocess_check_call
def test_create_link_mac_success():
old_platform = layout_tool.PLATFORM_NAME
old_subprocess_check_call = subprocess.check_call
try:
layout_tool.PLATFORM_NAME = 'Darwin'
src = "test_src"
dst = "test_dst"
expected = ['ln', '-s', src, dst]
def _mock_subprocess_check_call(args, stdout=None, stderr=None):
assert args == expected
subprocess.check_call = _mock_subprocess_check_call
layout_tool.create_link(src, dst, False)
finally:
layout_tool.PLATFORM_NAME = old_platform
subprocess.check_call = old_subprocess_check_call
def test_create_link_error():
old_platform = layout_tool.PLATFORM_NAME
old_subprocess_check_call = subprocess.check_call
try:
layout_tool.PLATFORM_NAME = 'Windows'
src = "test_src"
dst = "test_dst"
def _mock_subprocess_check_call(args, stdout=None, stderr=None):
raise subprocess.CalledProcessError(1, "Bad Call")
subprocess.check_call = _mock_subprocess_check_call
layout_tool.create_link(src, dst, False)
except common.LmbrCmdError:
pass
else:
assert False, "subprocess.CalledProcessError exception expected"
finally:
layout_tool.PLATFORM_NAME = old_platform
subprocess.check_call = old_subprocess_check_call
@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'),
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', 'dev/bootstrap.cfg', False, common.LmbrCmdError),
pytest.param('Foo', 'pc', 'dev/Cache/Foo/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):
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:
expected_path_realpath = common.LmbrCmdError
else:
expected_path_realpath = None
try:
result = layout_tool.construct_and_validate_cache_game_asset_folder(dev_root=dev_root_realpath,
game_name=game_name,
asset_type=asset_type,
warn_on_missing_game_cache=warn_on_missing)
assert expected_result != common.LmbrCmdError, "Expecting an error result"
if result == None:
assert warn_on_missing, "Expecting a warn_on_missing==True if None is returned"
elif isinstance(expected_result, str):
assert os.path.normcase(result) == os.path.normcase(expected_path_realpath)
except common.LmbrCmdError:
assert expected_result == common.LmbrCmdError
@pytest.mark.parametrize(
"existing_temp_vfs_folder, existing_gems_link, existing_game_link", [
pytest.param(False, False, False),
pytest.param(True, False, False),
pytest.param(False, True, False),
pytest.param(True, True, False),
pytest.param(False, False, True),
pytest.param(True, False, True),
pytest.param(False, True, True),
pytest.param(True, True, True)
]
)
def test_sync_layout_vfs_success(tmpdir, existing_temp_vfs_folder, existing_gems_link, existing_game_link):
old_tempfile_gettempdir = tempfile.gettempdir
old_create_link = layout_tool.create_link
old_copy_asset_files_to_layout = layout_tool.copy_asset_files_to_layout
old_rmdir = os.rmdir
old_unlink = os.unlink
try:
# Simple Test Parameters
test_dev_root = str(tmpdir.join('dev').realpath())
test_game = '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_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)
# If we are optionally testing existing links in a layout folder, track the expected and actual rmdirs
actual_rmdir_paths = set()
expected_rmdir_paths = set()
# The rmdir will serve as a wrapper to track the paths that are actually deleted
def _mock_os_rmdir(path):
actual_rmdir_paths.add(os.path.normcase(path))
old_rmdir(path)
def _mock_os_unlink(link):
actual_rmdir_paths.add(os.path.normcase(link))
if existing_gems_link:
# Optionally make a dummy folder for the target layout for gems and add it to the expected folder to delete
os.makedirs(layout_target_gems_realpath, exist_ok=False)
expected_rmdir_paths.add(os.path.normcase(layout_target_gems_realpath))
os.rmdir = _mock_os_rmdir
os.unlink = _mock_os_unlink
if existing_game_link:
# Optionally make a dummy folder for the target layout for the game folder and add it to the expected folder to delete
os.makedirs(layout_target_game_realpath, exist_ok=False)
expected_rmdir_paths.add(os.path.normcase(layout_target_game_realpath))
os.rmdir = _mock_os_rmdir
os.unlink = _mock_os_unlink
def _mock_gettempdir():
# mock tempfile.gettempdir() to use tmpdir from pytest
return str(tmpdir.realpath())
tempfile.gettempdir = _mock_gettempdir
# Predict the temp folder name
hasher = hashlib.md5()
hasher.update(test_dev_root.encode('UTF-8'))
hasher.update(game_folder.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())
test_layout_config_folder = str(tmpdir.join('{}/vfs/foo/config'.format(tmp_folder_subfolder)).realpath())
test_override_pak_folder = ''
if existing_temp_vfs_folder:
# Optionally make a dummy folder for the temp vfs and add the test layout folder and its child config to the expected folders to delete
os.makedirs(test_layout_config_folder, exist_ok=False)
expected_rmdir_paths.add(os.path.normcase(test_layout_folder))
expected_rmdir_paths.add(os.path.normcase(test_layout_config_folder))
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)
}
def _mock_layout_tool_create_link(src, dst, copy):
check_src = os.path.normcase(src)
check_dst = os.path.normcase(dst)
assert check_src in mock_layout_tool_create_link_validation, "Unexpected create link call to {}->{}".format(src, dst)
assert mock_layout_tool_create_link_validation[check_src] == check_dst, "Assertion on create linked failed: {}->{}".format(src, dst)
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):
# 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,
asset_type = test_asset_type,
warning_on_missing_assets = False,
layout_target = layout_target_root_realpath,
override_pak_folder = test_override_pak_folder,
copy = False)
# Verify if any the rmdir calls based on the test parameters
assert actual_rmdir_paths == expected_rmdir_paths
finally:
tempfile.gettempdir = old_tempfile_gettempdir
layout_tool.create_link = old_create_link
layout_tool.copy_asset_files_to_layout = old_copy_asset_files_to_layout
os.rmdir = old_rmdir
os.unlink = old_unlink
@pytest.mark.parametrize(
"mode, existing_game_link, existing_gems_link, test_override_pak_folder", [
pytest.param("LOOSE", False, False, None),
pytest.param("LOOSE", False, True, None),
pytest.param("LOOSE", True, False, None),
pytest.param("LOOSE", True, True, None),
pytest.param("PAK", False, None, None),
pytest.param("PAK", True, None, None),
pytest.param("PAK", False, None, 'override_paks'),
pytest.param("PAK", True, None, 'override_paks')
]
)
def test_sync_layout_non_vfs_success(tmpdir, mode, existing_game_link, existing_gems_link, test_override_pak_folder):
old_rmdir = os.rmdir
old_copy_asset_files_to_layout = layout_tool.copy_asset_files_to_layout
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'
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)
# 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)
# If we are optionally testing existing links in a layout folder, track the expected and actual rmdirs
actual_rmdir_paths = set()
expected_rmdir_paths = set()
def _mock_remove_link(link):
actual_rmdir_paths.add(os.path.normcase(link))
layout_tool.remove_link = _mock_remove_link
# The rmdir will serve as a wrapper to track the paths that are actually deleted
def _mock_os_rmdir(path):
actual_rmdir_paths.add(os.path.normcase(path))
old_rmdir(path)
if existing_game_link:
# Optionally make a dummy folder for the target layout for the game folder and add it to the expected folder to delete
os.makedirs(test_layout_target_game_realpath, exist_ok=False)
expected_rmdir_paths.add(os.path.normcase(test_layout_target_game_realpath))
os.rmdir = _mock_os_rmdir
mock_layout_tool_create_link_validation = {}
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)
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)
mock_layout_tool_create_link_validation[os.path.normcase(cache_game_folder_game_realpath)] = os.path.normcase(test_layout_target_game_realpath)
elif mode == "LOOSE":
# In LOOSE Mode, both game and gems will be linked
if existing_gems_link:
# Optionally make a dummy folder for the target layout for gems and add it to the expected folder to delete
os.makedirs(test_layout_target_gems_realpath, exist_ok=False)
expected_rmdir_paths.add(os.path.normcase(test_layout_target_gems_realpath))
os.rmdir = _mock_os_rmdir
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)
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)
else:
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)
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
def _mock_layout_tool_create_link(src, dst, copy):
check_src = os.path.normcase(src)
check_dst = os.path.normcase(dst)
assert check_src in mock_layout_tool_create_link_validation, "Unexpected create link call to {}->{}".format(src, dst)
assert mock_layout_tool_create_link_validation[check_src] == check_dst, "Assertion on create linked failed: {}->{}".format(src, dst)
layout_tool.create_link = _mock_layout_tool_create_link
layout_tool.sync_layout_non_vfs(mode = mode,
target_platform = test_target_platform,
dev_root = dev_root_realpath,
game = test_game,
asset_type = test_asset_type,
warning_on_missing_assets = False,
layout_target = test_layout_target_realpath,
override_pak_folder = test_override_pak_folder,
copy = False)
assert actual_rmdir_paths == expected_rmdir_paths
pass
finally:
os.rmdir = old_rmdir
layout_tool.copy_asset_files_to_layout = old_copy_asset_files_to_layout
layout_tool.remove_link = old_remove_link
+42
View File
@@ -0,0 +1,42 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
import pytest
from . import utils
@pytest.mark.parametrize(
"value, expected_result", [
pytest.param('Game1', True),
pytest.param('0Game1', False),
pytest.param('the/Game1', False),
pytest.param('', False),
pytest.param('-test', False),
pytest.param('test-', True),
]
)
def test_validate_identifier(value, expected_result):
result = utils.validate_identifier(value)
assert result == expected_result
@pytest.mark.parametrize(
"value, expected_result", [
pytest.param('{018427ae-cd08-4ff1-ad3b-9b95256c17ca}', False),
pytest.param('', False),
pytest.param('{018427aecd084ff1ad3b9b95256c17ca}', False),
pytest.param('018427ae-cd08-4ff1-ad3b-9b95256c17ca', True),
pytest.param('018427aecd084ff1ad3b9b95256c17ca', False),
pytest.param('018427aecd084ff1ad3b9', False),
]
)
def test_validate_uuid4(value, expected_result):
result = utils.validate_uuid4(value)
assert result == expected_result
+47
View File
@@ -0,0 +1,47 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
"""
This file contains utility functions
"""
import uuid
def validate_identifier(identifier: str) -> bool:
"""
Determine if the identifier supplied is valid.
:param identifier: the name which needs to to checked
:return: bool: if the identifier is valid or not
"""
if not identifier:
return False
elif len(identifier) > 64:
return False
elif not identifier[0].isalpha():
return False
else:
for character in identifier:
if not (character.isalnum() or character == '_' or character == '-'):
return False
return True
def validate_uuid4(uuid_string: str) -> bool:
"""
Determine if the uuid supplied is valid.
:param uuid_string: the uuid which needs to to checked
:return: bool: if the uuid is valid or not
"""
try:
val = uuid.UUID(uuid_string, version=4)
except ValueError:
return False
return str(val) == uuid_string