You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
641 lines
31 KiB
Python
641 lines
31 KiB
Python
#
|
|
# Copyright (c) Contributors to the Open 3D Engine Project.
|
|
# For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
#
|
|
#
|
|
|
|
|
|
import argparse
|
|
import hashlib
|
|
import logging
|
|
import os
|
|
import pathlib
|
|
import platform
|
|
import shutil
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import timeit
|
|
|
|
|
|
# Resolve the common python module
|
|
ROOT_ENGINE_PATH = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
|
if ROOT_ENGINE_PATH not in sys.path:
|
|
sys.path.append(ROOT_ENGINE_PATH)
|
|
|
|
from cmake.Tools import common
|
|
|
|
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 deny from copying to the layout folder
|
|
COPY_ASSET_FILE_GENERAL_DENYLIST_FILES = [
|
|
'aztest_bootstrap.json',
|
|
'editor.cfg',
|
|
'assetprocessorplatformconfig.setreg',
|
|
]
|
|
|
|
def verify_layout(layout_dir, platform_name, project_path, asset_mode, asset_type):
|
|
"""
|
|
Verify a layout folder (WRT to assets and configs) against the bootstrap and system config files
|
|
|
|
@param layout_dir: The layout path to validate the asset mode against the bootstrap and system configs
|
|
@param platform_name: The name of the platform the deployment is for
|
|
@param project_path: The path to the project being deployed
|
|
@param asset_mode: The desired asset mode (PAK, LOOSE, VFS)
|
|
@param asset_type: The asset type
|
|
@return: The number of possible errors in the configuration files based on the asset mode and type
|
|
"""
|
|
|
|
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.setreg' 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.setreg' is not configured for a remote Asset Processor connection (remote_ip={input_remote_ip})")
|
|
if input_remote_connect != '1':
|
|
return _warn(f"'bootstrap.setreg' 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.setreg' is not configured for a remote Asset Processor connection ({platform_name}_connect_to_remote={input_remote_connect}")
|
|
|
|
return 0
|
|
|
|
warning_count = 0
|
|
|
|
# Look up the project_path from the project.json file
|
|
project_name = common.read_project_name_from_project_json(project_path)
|
|
# If the project-name could not be read from the project.json, then the supplied project path does not
|
|
# point to a valid project
|
|
if not project_name:
|
|
return 1
|
|
|
|
platform_name_lower = platform_name.lower()
|
|
project_name_lower = project_path.lower()
|
|
layout_path = pathlib.Path(layout_dir)
|
|
|
|
bootstrap_path = pathlib.Path(ROOT_ENGINE_PATH) / 'Registry'
|
|
bootstrap_values = common.get_bootstrap_values(str(bootstrap_path), [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_lower}_{asset_type}.cfg'
|
|
if not platform_system_cfg_file.is_file():
|
|
warning_count += _warn(f"'system_{platform_name_lower}_{asset_type}.cfg' is missing from {str(layout_path)}")
|
|
system_config_values = None
|
|
else:
|
|
system_config_values = common.get_config_file_values(str(platform_system_cfg_file), [])
|
|
|
|
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_lower}_connect_to_remote') or '0'
|
|
|
|
# Validate that the asset type for the platform matches the one set for the build
|
|
bootstrap_asset_type = bootstrap_values.get(f'{platform_name_lower}_assets') or bootstrap_values.get('assets')
|
|
if not bootstrap_asset_type:
|
|
warning_count += _warn("'bootstrap.setreg' is missing specifications for asset type.")
|
|
elif bootstrap_asset_type != asset_type:
|
|
warning_count += _warn(f"The asset type specified in bootstrap.setreg ({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)
|
|
|
|
project_asset_path = layout_path / project_name_lower
|
|
if not project_asset_path.is_dir():
|
|
warning_count += _warn(f"Asset folder for project {project_name} is missing from the deployment layout.")
|
|
|
|
elif system_config_values is not None:
|
|
|
|
shaders_remote_compiler = '0'
|
|
asset_processor_shader_compiler = system_config_values.get('r_AssetProcessorShaderCompiler') or '0'
|
|
shader_compiler_server = LOCAL_HOST
|
|
shaders_allow_compilation = system_config_values.get('r_ShadersAllowCompilation')
|
|
|
|
def _validate_remote_shader_settings():
|
|
|
|
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
|
|
project_paks = project_asset_path.glob("*.pak")
|
|
for project_pak in project_paks:
|
|
if project_pak.name == 'shadercachestartup.pak':
|
|
has_shader_pak = True
|
|
pak_count += 1
|
|
|
|
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:
|
|
# 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 project {project_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.setreg 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(project_asset_folder, target_platform, layout_target):
|
|
"""
|
|
Perform the specific rules for copying files to the root level of the layout.
|
|
|
|
:param project_asset_folder: The source project asset folder to copy the files. (Will not traverse deeper than this folder)
|
|
:param target_platform: The target platform of the layout
|
|
:param layout_target: The target path of the target layout folder.
|
|
"""
|
|
|
|
src_asset_contents = os.listdir(project_asset_folder)
|
|
allowed_system_config_prefix = 'system_{}'.format(target_platform.lower())
|
|
for src_file in src_asset_contents:
|
|
|
|
# For each source file found in the root of the source project asset folder, apply various rules to determine
|
|
# if we will copy the file to the layout destination or not
|
|
if src_file in COPY_ASSET_FILE_GENERAL_DENYLIST_FILES:
|
|
# The source file is denied 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(project_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
|
|
|
|
logging.debug("Copying %s -> %s", abs_src, abs_dst)
|
|
shutil.copy2(abs_src, abs_dst)
|
|
|
|
|
|
def remove_link(link:pathlib.PurePath):
|
|
"""
|
|
Helper function to either remove a symlink, or remove a folder
|
|
"""
|
|
link = pathlib.PurePath(link)
|
|
if os.path.isdir(link):
|
|
try:
|
|
os.unlink(link)
|
|
except OSError:
|
|
# If unlink fails use shutil.rmtree
|
|
def remove_readonly(func, path, _):
|
|
"Clear the readonly bit and reattempt the removal"
|
|
os.chmod(path, stat.S_IWRITE)
|
|
func(path)
|
|
|
|
try:
|
|
shutil.rmtree(link, onerror=remove_readonly)
|
|
except shutil.Error as shutil_error:
|
|
raise common.LmbrCmdError(f'Error trying remove directory {link}: {shutil_error}', shutil_error.errno)
|
|
|
|
|
|
def create_link(src:pathlib.Path, tgt:pathlib.Path, copy):
|
|
"""
|
|
Helper function to create a directory link or copy a directory. On windows, this will be a directory junction, and on mac/linux
|
|
this will be a soft link
|
|
|
|
: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
|
|
"""
|
|
src = pathlib.Path(src)
|
|
tgt = pathlib.Path(tgt)
|
|
if copy:
|
|
# Remove the exist target
|
|
if tgt.exists():
|
|
if tgt.is_symlink():
|
|
tgt.unlink()
|
|
else:
|
|
def remove_readonly(func, path, _):
|
|
"Clear the readonly bit and reattempt the removal"
|
|
os.chmod(path, stat.S_IWRITE)
|
|
func(path)
|
|
shutil.rmtree(tgt, onerror=remove_readonly)
|
|
|
|
logging.debug(f'Copying from {src} to {tgt}')
|
|
shutil.copytree(str(src), str(tgt), symlinks=False)
|
|
else:
|
|
link_type = "symlink"
|
|
logging.debug(f'Creating symlink {src} =>{tgt}')
|
|
try:
|
|
if PLATFORM_NAME == "Windows":
|
|
link_type = "junction"
|
|
import _winapi
|
|
_winapi.CreateJunction(str(src), str(tgt))
|
|
else:
|
|
if tgt.exists():
|
|
tgt.unlink()
|
|
tgt.symlink_to(src, target_is_directory=True)
|
|
except OSError as e:
|
|
raise common.LmbrCmdError(f"Error trying to create {link_type} {src} => {tgt} : {e}", e.errno)
|
|
|
|
|
|
def construct_and_validate_cache_project_asset_folder(project_path, asset_type, warn_on_missing_project_cache):
|
|
"""
|
|
Given the parameters for a project (project_path, asset type), construct and validate the absolute path
|
|
of where the built assets are (for LOOSE and VFS modes)
|
|
|
|
:param project_path: The path to the project
|
|
:param asset_type: The type of asset
|
|
:param warn_on_missing_project_cache: Option to warn if the path is missing vs raising an exception
|
|
:return: The validated constructed cache project asset folder if it exists, None if not
|
|
"""
|
|
# Locate the Cache root folder
|
|
cache_project_folder_root = os.path.join(project_path, CACHE_FOLDER_NAME)
|
|
if not os.path.isdir(cache_project_folder_root) and not warn_on_missing_project_cache:
|
|
raise common.LmbrCmdError(
|
|
f"Missing Cache folder for the project at path {project_path}. Make sure that assets have been built ",
|
|
common.ERROR_CODE_ERROR_DIRECTORY)
|
|
|
|
# Locate based on the project's built asset type
|
|
cache_project_asset_folder = os.path.join(cache_project_folder_root, asset_type)
|
|
if os.path.isdir(cache_project_asset_folder):
|
|
# TODO: Note, this is only checking the existence of the folder, not for any content validation
|
|
return cache_project_asset_folder
|
|
|
|
# Expected source of the project assets was not found
|
|
if not warn_on_missing_project_cache:
|
|
raise common.LmbrCmdError(
|
|
f'Missing compiled assets folder for the project at path {project_path}."'
|
|
f' Make sure that assets for "{asset_type}" have been built',
|
|
common.ERROR_CODE_ERROR_DIRECTORY)
|
|
|
|
return None
|
|
|
|
|
|
def sync_layout_vfs(target_platform, project_path, asset_type, warning_on_missing_assets, layout_target, override_pak_folder, copy):
|
|
"""
|
|
Perform the logic to sync the layout folder with assets in VFS mode
|
|
|
|
:param target_platform: The target platform the layout is based on
|
|
:param project_path: The path to the project being synced
|
|
:param asset_type: The asset type being synced
|
|
:param warning_on_missing_assets: If the built assets cannot be located (LOOSE or PAKs), then optionally warn vs raising an error
|
|
:param layout_target: The target layout folder to perform the sync on
|
|
: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(f'Syncing VFS layout for project at path "{project_path}" to layout path "{layout_target}"')
|
|
|
|
project_asset_folder = construct_and_validate_cache_project_asset_folder(project_path=project_path,
|
|
asset_type=asset_type,
|
|
warn_on_missing_project_cache=warning_on_missing_assets)
|
|
|
|
vfs_asset_source = os.path.join(project_asset_folder, 'config')
|
|
if not os.path.isdir(vfs_asset_source):
|
|
raise common.LmbrCmdError("Cache folder for the project '{}' missing 'config' folder".format(project_path),
|
|
common.ERROR_CODE_ERROR_DIRECTORY)
|
|
|
|
# create a temporary folder that will serve as a working junction point into the layout
|
|
hasher = hashlib.md5()
|
|
hasher.update(project_path.encode('UTF-8'))
|
|
result = hasher.hexdigest()
|
|
|
|
temp_dir = tempfile.gettempdir()
|
|
temp_vfs_layout_path = os.path.join(temp_dir, 'ly-layout-{}'.format(result), 'vfs')
|
|
temp_vfs_layout_project_path = temp_vfs_layout_path
|
|
|
|
temp_vfs_layout_project_config_path = os.path.join(temp_vfs_layout_project_path, 'config')
|
|
|
|
# If the temporary folder was created previously, always reset it
|
|
if os.path.isdir(temp_vfs_layout_project_path):
|
|
if os.path.isdir(temp_vfs_layout_project_config_path):
|
|
os.rmdir(temp_vfs_layout_project_config_path)
|
|
shutil.rmtree(temp_vfs_layout_project_path)
|
|
os.makedirs(temp_vfs_layout_project_path, exist_ok=True)
|
|
|
|
# Create the 'project asset platform cache' junction before copying configuration files at the engine root to it
|
|
layout_project_folder_target = layout_target
|
|
# Remove previous layout folder if it is a directory
|
|
if os.path.isdir(layout_project_folder_target):
|
|
remove_link(layout_project_folder_target)
|
|
if os.path.isdir(temp_vfs_layout_project_path):
|
|
create_link(temp_vfs_layout_project_path, layout_project_folder_target, copy)
|
|
|
|
# Create the link
|
|
create_link(vfs_asset_source, temp_vfs_layout_project_config_path, copy)
|
|
|
|
# Create the assets to the layout
|
|
copy_asset_files_to_layout(project_asset_folder=project_asset_folder,
|
|
target_platform=target_platform,
|
|
layout_target=layout_target)
|
|
|
|
# Reset the 'gems' junction if any in the layout
|
|
layout_gems_folder_src = os.path.join(project_asset_folder, 'gems')
|
|
layout_gems_folder_target = os.path.join(layout_target, 'gems')
|
|
if os.path.isdir(layout_gems_folder_target):
|
|
remove_link(layout_gems_folder_target)
|
|
if os.path.isdir(layout_gems_folder_src):
|
|
create_link(layout_gems_folder_src, layout_gems_folder_target, copy)
|
|
|
|
|
|
|
|
def sync_layout_non_vfs(mode, target_platform, project_path, asset_type, warning_on_missing_assets, layout_target, override_pak_folder, copy):
|
|
"""
|
|
Perform the logic to sync the layout folder with assets in non-VFS mode (LOOSE or PAK)
|
|
|
|
:param mode: 'LOOSE' or 'PAK' mode
|
|
:param target_platform: The target platform the layout is based on
|
|
:param project_path: The path to the project being synced
|
|
:param asset_type: The asset type being synced
|
|
:param warning_on_missing_assets: If the built assets cannot be located (LOOSE or PAKs), then optionally warn vs raising an error
|
|
:param layout_target: The target layout folder to perform the sync on
|
|
: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)
|
|
|
|
project_name = common.read_project_name_from_project_json(project_path)
|
|
if not project_name:
|
|
raise common.LmbrCmdError(f'Project at path {project_path} does not have a valid project.json')
|
|
|
|
project_name_lower = project_name.lower()
|
|
layout_gems_folder_target = os.path.join(layout_target, 'gems')
|
|
if os.path.isdir(layout_gems_folder_target):
|
|
remove_link(layout_gems_folder_target)
|
|
|
|
if mode == ASSET_MODE_PAK:
|
|
target_pak_folder_name = '{}_{}_paks'.format(project_name_lower, asset_type)
|
|
project_asset_folder = os.path.join(project_path, override_pak_folder or PAK_FOLDER_NAME, target_pak_folder_name)
|
|
if not os.path.isdir(project_asset_folder):
|
|
if warning_on_missing_assets:
|
|
logging.warning(f'Pak folder for the project at path "{project_path}" is missing'
|
|
f' (expected at "{project_asset_folder}"). Skipping layout sync')
|
|
return
|
|
else:
|
|
raise common.LmbrCmdError(f'Pak folder for the project at path "{project_path}" is missing (expected at'
|
|
f' "{project_asset_folder}")',
|
|
common.ERROR_CODE_ERROR_DIRECTORY)
|
|
|
|
elif mode == ASSET_MODE_LOOSE:
|
|
project_asset_folder = construct_and_validate_cache_project_asset_folder(project_path=project_path,
|
|
asset_type=asset_type,
|
|
warn_on_missing_project_cache=warning_on_missing_assets)
|
|
if not project_asset_folder:
|
|
logging.warning(
|
|
f'Cannot locate built assets for project at path "{project_path}" (expected at "{project_asset_folder}").'
|
|
f' Skipping layout sync')
|
|
return
|
|
else:
|
|
assert False, "Invalid Mode {}".format(mode)
|
|
|
|
# Create the 'project asset platform cache' junction before copying additional files to it
|
|
layout_project_folder_src = project_asset_folder
|
|
# Remove previous layout folder if it is a directory
|
|
if os.path.isdir(layout_target):
|
|
remove_link(layout_target)
|
|
if os.path.isdir(layout_project_folder_src):
|
|
create_link(layout_project_folder_src, layout_target, copy)
|
|
|
|
# Create the assets to the layout
|
|
copy_asset_files_to_layout(project_asset_folder=project_asset_folder,
|
|
target_platform=target_platform,
|
|
layout_target=layout_target)
|
|
|
|
# Reset the 'gems' junction if any in the layout (only in loose mode).
|
|
layout_gems_folder_src = os.path.join(project_asset_folder, 'gems')
|
|
|
|
# The gems link only is valid in LOOSE mode. If in PAK, then dont re-link
|
|
if mode == ASSET_MODE_LOOSE and os.path.isdir(layout_gems_folder_src):
|
|
if os.path.isdir(layout_gems_folder_src):
|
|
create_link(layout_gems_folder_src, layout_gems_folder_target, copy)
|
|
|
|
|
|
def sync_layout_pak(target_platform, project_path, asset_type, warning_on_missing_assets, layout_target,
|
|
override_pak_folder, copy):
|
|
sync_layout_non_vfs(mode=ASSET_MODE_PAK,
|
|
target_platform=target_platform,
|
|
project_path=project_path,
|
|
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(target_platform, project_path, asset_type, warning_on_missing_assets, layout_target,
|
|
override_pak_folder, copy):
|
|
sync_layout_non_vfs(mode=ASSET_MODE_LOOSE,
|
|
target_platform=target_platform,
|
|
project_path=project_path,
|
|
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 project's assets to a layout folder")
|
|
|
|
parser.add_argument('--project-path',
|
|
help='The project path whose assets we will sync.',
|
|
required=True)
|
|
parser.add_argument('-p', '--platform',
|
|
help='Target platform for the layout.',
|
|
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 project 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 {project_path}/pak/${project}_${asset_type}_pak as the source pak folder')
|
|
parser.add_argument('--build-config',
|
|
default='',
|
|
help='(optional) If provided, will adjust the asset mode if the provided build-config is "release"')
|
|
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)
|
|
|
|
# Prepare the logging
|
|
logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG if parsed_args.debug else logging.INFO)
|
|
|
|
# Validate the asset mode
|
|
input_asset_mode = parsed_args.mode.upper()
|
|
if input_asset_mode not in ALL_ASSET_MODES:
|
|
raise common.LmbrCmdError("Invalid asset mode '{}'. Must be one of : '{}'.".format(input_asset_mode, ','.join(ALL_ASSET_MODES)),
|
|
common.ERROR_CODE_INVALID_PARAMETER)
|
|
|
|
# Check if the build config is set, if so, check if its release
|
|
build_config = parsed_args.build_config.upper()
|
|
if build_config in PAK_ONLY_BUILD_CONFIGS:
|
|
input_asset_mode = ASSET_MODE_PAK
|
|
|
|
logging.info("Starting (%s) Asset Synchronization in %s mode and project %s", parsed_args.asset_type, input_asset_mode, parsed_args.project_path)
|
|
start_time = timeit.default_timer()
|
|
ASSET_SYNC_MODE_FUNCTION[input_asset_mode](target_platform=parsed_args.platform,
|
|
project_path=parsed_args.project_path,
|
|
asset_type=parsed_args.asset_type,
|
|
warning_on_missing_assets=parsed_args.warn_on_missing_assets,
|
|
layout_target=os.path.normpath(parsed_args.layout_root),
|
|
override_pak_folder=parsed_args.override_pak_folder,
|
|
copy=parsed_args.copy)
|
|
duration = timeit.default_timer() - start_time
|
|
logging.info("Asset Synchronization complete {:.2f} seconds".format(duration))
|
|
|
|
# Remove broken symlinks/junctions to the layout folder
|
|
if os.path.isdir(parsed_args.layout_root) and not os.path.exists(parsed_args.layout_root):
|
|
remove_link(parsed_args.layout_root)
|
|
|
|
if not os.path.isdir(parsed_args.layout_root):
|
|
# If the layout target doesnt exist, check if we want to create it
|
|
if parsed_args.create_layout_root:
|
|
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)
|
|
|
|
if parsed_args.verify:
|
|
warnings = verify_layout(layout_dir=os.path.normpath(parsed_args.layout_root),
|
|
platform_name=parsed_args.platform,
|
|
project_path=parsed_args.project_path,
|
|
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)
|