layout_tool.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. #
  2. # Copyright (c) Contributors to the Open 3D Engine Project.
  3. # For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. #
  5. # SPDX-License-Identifier: Apache-2.0 OR MIT
  6. #
  7. #
  8. import argparse
  9. import hashlib
  10. import logging
  11. import os
  12. import pathlib
  13. import platform
  14. import shutil
  15. import stat
  16. import subprocess
  17. import sys
  18. import tempfile
  19. import timeit
  20. # Resolve the common python module
  21. ROOT_ENGINE_PATH = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', '..'))
  22. if ROOT_ENGINE_PATH not in sys.path:
  23. sys.path.append(ROOT_ENGINE_PATH)
  24. from cmake.Tools import common
  25. LOCAL_HOST = '127.0.0.1'
  26. CACHE_FOLDER_NAME = 'Cache'
  27. ASSET_MODE_PAK = 'PAK'
  28. ASSET_MODE_LOOSE = 'LOOSE'
  29. ASSET_MODE_VFS = 'VFS'
  30. ALL_ASSET_MODES = [ASSET_MODE_PAK, ASSET_MODE_LOOSE, ASSET_MODE_VFS]
  31. PAK_FOLDER_NAME = 'Pak'
  32. # Maintain a list of build configs that only will support PAK mode
  33. PAK_ONLY_BUILD_CONFIGS = ['RELEASE']
  34. # Save the platform system name. In our case, this will be one of:
  35. # Windows
  36. # Linux
  37. # Darwin (Currently 'Darwin' with python 3.10). Should use 'Windows' and 'Linux' first and fallback to Darwin
  38. PLATFORM_NAME = platform.system()
  39. # List of files to deny from copying to the layout folder
  40. COPY_ASSET_FILE_GENERAL_DENYLIST_FILES = [
  41. 'aztest_bootstrap.json',
  42. 'editor.cfg',
  43. 'assetprocessorplatformconfig.setreg',
  44. ]
  45. def verify_layout(layout_dir, platform_name, project_path, asset_mode, asset_type):
  46. """
  47. Verify a layout folder (WRT to assets and configs) against the bootstrap and system config files
  48. @param layout_dir: The layout path to validate the asset mode against the bootstrap and system configs
  49. @param platform_name: The name of the platform the deployment is for
  50. @param project_path: The path to the project being deployed
  51. @param asset_mode: The desired asset mode (PAK, LOOSE, VFS)
  52. @param asset_type: The asset type
  53. @return: The number of possible errors in the configuration files based on the asset mode and type
  54. """
  55. def _warn(msg):
  56. logging.warning(msg)
  57. return 1
  58. def _validate_remote_ap(input_remote_ip, input_remote_connect, remote_on_check):
  59. if remote_on_check is None:
  60. # Validate that if '<platform>_connect_to_remote is enabled, that the 'input_remote_ip' is not set to local host
  61. if input_remote_connect == '1' and input_remote_ip == LOCAL_HOST:
  62. return _warn("'bootstrap.setreg' is configured to connect to Asset Processor remotely, but the 'remote_ip' "
  63. " is configured for LOCAL HOST")
  64. else:
  65. if remote_on_check:
  66. # Verify we are set for remote AP connection
  67. if input_remote_ip == LOCAL_HOST:
  68. return _warn(f"'bootstrap.setreg' is not configured for a remote Asset Processor connection (remote_ip={input_remote_ip})")
  69. if input_remote_connect != '1':
  70. return _warn(f"'bootstrap.setreg' is not configured for a remote Asset Processor connection ({platform_name}_connect_to_remote={input_remote_connect}")
  71. else:
  72. # Verify we are disabled for remote AP connection
  73. if input_remote_connect != '0':
  74. return _warn(f"'bootstrap.setreg' is not configured for a remote Asset Processor connection ({platform_name}_connect_to_remote={input_remote_connect}")
  75. return 0
  76. warning_count = 0
  77. # Look up the project_path from the project.json file
  78. project_name = common.read_project_name_from_project_json(project_path)
  79. # If the project-name could not be read from the project.json, then the supplied project path does not
  80. # point to a valid project
  81. if not project_name:
  82. return 1
  83. platform_name_lower = platform_name.lower()
  84. project_name_lower = project_path.lower()
  85. layout_path = pathlib.Path(layout_dir)
  86. bootstrap_path = pathlib.Path(ROOT_ENGINE_PATH) / 'Registry'
  87. bootstrap_values = common.get_bootstrap_values(str(bootstrap_path), [f'{platform_name_lower}_remote_filesystem',
  88. f'{platform_name_lower}_connect_to_remote',
  89. f'{platform_name_lower}_wait_for_connect',
  90. f'{platform_name_lower}_assets',
  91. f'assets',
  92. f'{platform_name_lower}_remote_ip',
  93. f'remote_ip'
  94. ])
  95. if bootstrap_values:
  96. remote_ip = bootstrap_values.get(f'{platform_name_lower}_remote_ip') or bootstrap_values.get('remote_ip') or LOCAL_HOST
  97. remote_connect = bootstrap_values.get(f'{platform_name_lower}_connect_to_remote') or '0'
  98. # Validate that the asset type for the platform matches the one set for the build
  99. bootstrap_asset_type = bootstrap_values.get(f'{platform_name_lower}_assets') or bootstrap_values.get('assets')
  100. if not bootstrap_asset_type:
  101. warning_count += _warn("'bootstrap.setreg' is missing specifications for asset type.")
  102. elif bootstrap_asset_type != asset_type:
  103. 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}).")
  104. # Validate that if '<platform>_connect_to_remote is enabled, that the 'remote_ip' is not set to local host
  105. warning_count += _validate_remote_ap(remote_ip, remote_connect, None)
  106. project_asset_path = layout_path
  107. if not project_asset_path.is_dir():
  108. warning_count += _warn(f"Asset folder for project {project_name} is missing from the deployment layout.")
  109. else:
  110. # Validation steps based on the asset mode
  111. if asset_mode == ASSET_MODE_PAK:
  112. # Validate that we have pak files
  113. project_paks = project_asset_path.glob("*.pak")
  114. pak_count = len(list(project_paks))
  115. if pak_count == 0:
  116. warning_count += _warn("No pak files found for PAK mode deployment")
  117. # Check if the shader paks are set
  118. # Since we are using pak files, make sure the settings are not configured to
  119. # connect to Asset Processor remotely
  120. warning_count += _validate_remote_ap(remote_ip, remote_connect, False)
  121. elif asset_mode == ASSET_MODE_VFS:
  122. remote_file_system = bootstrap_values.get(f'{platform_name_lower}_remote_filesystem') or '0'
  123. if not remote_file_system != '1':
  124. warning_count += _warn("Remote file system is not configured in bootstrap.setreg for VFS mode.")
  125. else:
  126. warning_count += _validate_remote_ap(remote_ip, remote_connect, True)
  127. return warning_count
  128. def copy_asset_files_to_layout(project_asset_folder, target_platform, layout_target):
  129. """
  130. Perform the specific rules for copying files to the root level of the layout.
  131. :param project_asset_folder: The source project asset folder to copy the files. (Will not traverse deeper than this folder)
  132. :param target_platform: The target platform of the layout
  133. :param layout_target: The target path of the target layout folder.
  134. """
  135. src_asset_contents = os.listdir(project_asset_folder)
  136. allowed_system_config_prefix = 'system_{}'.format(target_platform.lower())
  137. for src_file in src_asset_contents:
  138. # For each source file found in the root of the source project asset folder, apply various rules to determine
  139. # if we will copy the file to the layout destination or not
  140. if src_file in COPY_ASSET_FILE_GENERAL_DENYLIST_FILES:
  141. # The source file is denied from being copied
  142. continue
  143. if src_file.startswith('system_'):
  144. # For system files (system_<platform>_<asset_platform>), only allow the ones that are marked for the
  145. # current <platform>
  146. if not src_file.startswith(allowed_system_config_prefix):
  147. continue
  148. # Resolve the absolute paths for source and destination to perform more specific checks
  149. abs_src = os.path.join(project_asset_folder, src_file)
  150. abs_dst = os.path.join(layout_target, src_file)
  151. if os.path.isdir(abs_src):
  152. # Skip all source folders
  153. continue
  154. # The target file exists, check whats at the target
  155. if os.path.isdir(abs_dst):
  156. # The target destination is a folder, we will skip
  157. logging.warning("Skipping layout copying of file '%s' because the target '%s' refers to a directory",
  158. src_file,
  159. abs_dst)
  160. continue
  161. # If the target file doesn't exist, copy it
  162. if not os.path.exists(abs_dst):
  163. logging.debug("Copying %s -> %s", abs_src, abs_dst)
  164. shutil.copy2(abs_src, abs_dst)
  165. elif os.path.isfile(abs_dst):
  166. # The target is a file, do a fingerprint check
  167. # TODO: Evaluate if we want to just junction the files instead of doing a copy
  168. src_hash = common.file_fingerprint(abs_src)
  169. dst_hash = common.file_fingerprint(abs_dst)
  170. if src_hash == dst_hash:
  171. logging.debug("Skipping layout copy of '%s', fingerprints of source and destination matches (%s)",
  172. src_file,
  173. src_hash)
  174. continue
  175. logging.debug("Copying %s -> %s", abs_src, abs_dst)
  176. shutil.copy2(abs_src, abs_dst)
  177. def remove_link(link:pathlib.PurePath):
  178. """
  179. Helper function to either remove a symlink, or remove a folder
  180. """
  181. link = pathlib.PurePath(link)
  182. if os.path.isdir(link):
  183. try:
  184. os.unlink(link)
  185. except OSError:
  186. # If unlink fails use shutil.rmtree
  187. def remove_readonly(func, path, _):
  188. "Clear the readonly bit and reattempt the removal"
  189. os.chmod(path, stat.S_IWRITE)
  190. func(path)
  191. try:
  192. shutil.rmtree(link, onerror=remove_readonly)
  193. except shutil.Error as shutil_error:
  194. raise common.LmbrCmdError(f'Error trying remove directory {link}: {shutil_error}', shutil_error.errno)
  195. def create_link(src:pathlib.Path, tgt:pathlib.Path, copy):
  196. """
  197. Helper function to create a directory link or copy a directory. On windows, this will be a directory junction, and on mac/linux
  198. this will be a soft link
  199. :param src: The name of the link to create
  200. :param tgt: The target of the new link
  201. :param copy: Perform a directory copy instead of a link
  202. """
  203. src = pathlib.Path(src)
  204. tgt = pathlib.Path(tgt)
  205. if copy:
  206. # Remove the exist target
  207. if tgt.exists():
  208. if tgt.is_symlink():
  209. tgt.unlink()
  210. else:
  211. def remove_readonly(func, path, _):
  212. "Clear the readonly bit and reattempt the removal"
  213. os.chmod(path, stat.S_IWRITE)
  214. func(path)
  215. shutil.rmtree(tgt, onerror=remove_readonly)
  216. logging.debug(f'Copying from {src} to {tgt}')
  217. shutil.copytree(str(src), str(tgt), symlinks=False)
  218. else:
  219. link_type = "symlink"
  220. logging.debug(f'Creating symlink {src} =>{tgt}')
  221. try:
  222. if PLATFORM_NAME == "Windows":
  223. link_type = "junction"
  224. import _winapi
  225. _winapi.CreateJunction(str(src), str(tgt))
  226. else:
  227. if tgt.exists():
  228. tgt.unlink()
  229. tgt.symlink_to(src, target_is_directory=True)
  230. except OSError as e:
  231. raise common.LmbrCmdError(f"Error trying to create {link_type} {src} => {tgt} : {e}", e.errno)
  232. def construct_and_validate_cache_project_asset_folder(project_path, asset_type, warn_on_missing_project_cache):
  233. """
  234. Given the parameters for a project (project_path, asset type), construct and validate the absolute path
  235. of where the built assets are (for LOOSE and VFS modes)
  236. :param project_path: The path to the project
  237. :param asset_type: The type of asset
  238. :param warn_on_missing_project_cache: Option to warn if the path is missing vs raising an exception
  239. :return: The validated constructed cache project asset folder if it exists, None if not
  240. """
  241. # Locate the Cache root folder
  242. cache_project_folder_root = os.path.join(project_path, CACHE_FOLDER_NAME)
  243. if not os.path.isdir(cache_project_folder_root) and not warn_on_missing_project_cache:
  244. raise common.LmbrCmdError(
  245. f"Missing Cache folder for the project at path {project_path}. Make sure that assets have been built ",
  246. common.ERROR_CODE_ERROR_DIRECTORY)
  247. # Locate based on the project's built asset type
  248. cache_project_asset_folder = os.path.join(cache_project_folder_root, asset_type)
  249. if os.path.isdir(cache_project_asset_folder):
  250. # TODO: Note, this is only checking the existence of the folder, not for any content validation
  251. return cache_project_asset_folder
  252. # Expected source of the project assets was not found
  253. if not warn_on_missing_project_cache:
  254. raise common.LmbrCmdError(
  255. f'Missing compiled assets folder for the project at path {project_path}."'
  256. f' Make sure that assets for "{asset_type}" have been built',
  257. common.ERROR_CODE_ERROR_DIRECTORY)
  258. return None
  259. def sync_layout_vfs(target_platform, project_path, asset_type, warning_on_missing_assets, layout_target, override_pak_folder, copy):
  260. """
  261. Perform the logic to sync the layout folder with assets in VFS mode
  262. :param target_platform: The target platform the layout is based on
  263. :param project_path: The path to the project being synced
  264. :param asset_type: The asset type being synced
  265. :param warning_on_missing_assets: If the built assets cannot be located (LOOSE or PAKs), then optionally warn vs raising an error
  266. :param layout_target: The target layout folder to perform the sync on
  267. :param override_pak_folder: The optional path to override the default pak folder for PAK asset mode (N/A for this function)
  268. :param copy: Option to copy instead of attempting to symlink/junction
  269. """
  270. logging.debug(f'Syncing VFS layout for project at path "{project_path}" to layout path "{layout_target}"')
  271. project_asset_folder = construct_and_validate_cache_project_asset_folder(project_path=project_path,
  272. asset_type=asset_type,
  273. warn_on_missing_project_cache=warning_on_missing_assets)
  274. vfs_asset_source = os.path.join(project_asset_folder, 'config')
  275. if not os.path.isdir(vfs_asset_source):
  276. raise common.LmbrCmdError("Cache folder for the project '{}' missing 'config' folder".format(project_path),
  277. common.ERROR_CODE_ERROR_DIRECTORY)
  278. # create a temporary folder that will serve as a working junction point into the layout
  279. hasher = hashlib.md5()
  280. hasher.update(project_path.encode('UTF-8'))
  281. result = hasher.hexdigest()
  282. temp_dir = tempfile.gettempdir()
  283. temp_vfs_layout_path = os.path.join(temp_dir, 'ly-layout-{}'.format(result), 'vfs')
  284. temp_vfs_layout_project_path = temp_vfs_layout_path
  285. temp_vfs_layout_project_config_path = os.path.join(temp_vfs_layout_project_path, 'config')
  286. # If the temporary folder was created previously, always reset it
  287. if os.path.isdir(temp_vfs_layout_project_path):
  288. if os.path.isdir(temp_vfs_layout_project_config_path):
  289. os.rmdir(temp_vfs_layout_project_config_path)
  290. shutil.rmtree(temp_vfs_layout_project_path)
  291. os.makedirs(temp_vfs_layout_project_path, exist_ok=True)
  292. # Create the 'project asset platform cache' junction before copying configuration files at the engine root to it
  293. layout_project_folder_target = layout_target
  294. # Remove previous layout folder if it is a directory
  295. if os.path.isdir(layout_project_folder_target):
  296. remove_link(layout_project_folder_target)
  297. if os.path.isdir(temp_vfs_layout_project_path):
  298. create_link(temp_vfs_layout_project_path, layout_project_folder_target, copy)
  299. # Create the link
  300. create_link(vfs_asset_source, temp_vfs_layout_project_config_path, copy)
  301. # Copy minimum assets to the layout necessary for vfs
  302. root_assets = ['engine.json',
  303. 'bootstrap.client.debug.setreg', 'bootstrap.client.profile.setreg', 'bootstrap.client.release.setreg',
  304. 'bootstrap.server.debug.setreg', 'bootstrap.server.profile.setreg', 'bootstrap.server.release.setreg',
  305. 'bootstrap.unified.debug.setreg', 'bootstrap.unified.profile.setreg', 'bootstrap.unified.release.setreg']
  306. for root_asset in root_assets:
  307. logging.debug("Copying %s -> %s", os.path.join(project_asset_folder, root_asset), layout_target)
  308. shutil.copy2(os.path.join(project_asset_folder, root_asset), layout_target)
  309. # Reset the 'gems' junction if any in the layout
  310. layout_gems_folder_src = os.path.join(project_asset_folder, 'gems')
  311. layout_gems_folder_target = os.path.join(layout_target, 'gems')
  312. if os.path.isdir(layout_gems_folder_target):
  313. remove_link(layout_gems_folder_target)
  314. if os.path.isdir(layout_gems_folder_src):
  315. create_link(layout_gems_folder_src, layout_gems_folder_target, copy)
  316. def sync_layout_non_vfs(mode, target_platform, project_path, asset_type, warning_on_missing_assets, layout_target, override_pak_folder, copy):
  317. """
  318. Perform the logic to sync the layout folder with assets in non-VFS mode (LOOSE or PAK)
  319. :param mode: 'LOOSE' or 'PAK' mode
  320. :param target_platform: The target platform the layout is based on
  321. :param project_path: The path to the project being synced
  322. :param asset_type: The asset type being synced
  323. :param warning_on_missing_assets: If the built assets cannot be located (LOOSE or PAKs), then optionally warn vs raising an error
  324. :param layout_target: The target layout folder to perform the sync on
  325. :param override_pak_folder: The optional path to override the default pak folder for PAK asset mode (N/A for this function)
  326. :param copy: Option to copy instead of attempting to symlink/junction
  327. """
  328. assert mode in (ASSET_MODE_PAK, ASSET_MODE_LOOSE)
  329. project_name = common.read_project_name_from_project_json(project_path)
  330. if not project_name:
  331. raise common.LmbrCmdError(f'Project at path {project_path} does not have a valid project.json')
  332. project_name_lower = project_name.lower()
  333. layout_gems_folder_target = os.path.join(layout_target, 'gems')
  334. if os.path.isdir(layout_gems_folder_target):
  335. remove_link(layout_gems_folder_target)
  336. if mode == ASSET_MODE_PAK:
  337. target_pak_folder_name = '{}_{}_paks'.format(project_name_lower, asset_type)
  338. if override_pak_folder:
  339. project_asset_folder = override_pak_folder
  340. else:
  341. project_asset_folder = os.path.join(project_path, override_pak_folder or PAK_FOLDER_NAME, target_pak_folder_name)
  342. if not os.path.isdir(project_asset_folder):
  343. if warning_on_missing_assets:
  344. logging.warning(f'Pak folder for the project at path "{project_path}" is missing'
  345. f' (expected at "{project_asset_folder}"). Skipping layout sync')
  346. return
  347. else:
  348. raise common.LmbrCmdError(f'Pak folder for the project at path "{project_path}" is missing (expected at'
  349. f' "{project_asset_folder}")',
  350. common.ERROR_CODE_ERROR_DIRECTORY)
  351. elif mode == ASSET_MODE_LOOSE:
  352. project_asset_folder = construct_and_validate_cache_project_asset_folder(project_path=project_path,
  353. asset_type=asset_type,
  354. warn_on_missing_project_cache=warning_on_missing_assets)
  355. if not project_asset_folder:
  356. logging.warning(
  357. f'Cannot locate built assets for project at path "{project_path}" (expected at "{project_asset_folder}").'
  358. f' Skipping layout sync')
  359. return
  360. else:
  361. assert False, "Invalid Mode {}".format(mode)
  362. # Create the 'project asset platform cache' junction before copying additional files to it
  363. layout_project_folder_src = project_asset_folder
  364. # Remove previous layout folder if it is a directory
  365. if os.path.isdir(layout_target):
  366. remove_link(layout_target)
  367. if os.path.isdir(layout_project_folder_src):
  368. create_link(layout_project_folder_src, layout_target, copy)
  369. # Create the assets to the layout
  370. copy_asset_files_to_layout(project_asset_folder=project_asset_folder,
  371. target_platform=target_platform,
  372. layout_target=layout_target)
  373. # Reset the 'gems' junction if any in the layout (only in loose mode).
  374. layout_gems_folder_src = os.path.join(project_asset_folder, 'gems')
  375. # The gems link only is valid in LOOSE mode. If in PAK, then dont re-link
  376. if mode == ASSET_MODE_LOOSE and os.path.isdir(layout_gems_folder_src):
  377. if os.path.isdir(layout_gems_folder_src):
  378. create_link(layout_gems_folder_src, layout_gems_folder_target, copy)
  379. def sync_layout_pak(target_platform, project_path, asset_type, warning_on_missing_assets, layout_target,
  380. override_pak_folder, copy):
  381. sync_layout_non_vfs(mode=ASSET_MODE_PAK,
  382. target_platform=target_platform,
  383. project_path=project_path,
  384. asset_type=asset_type,
  385. warning_on_missing_assets=warning_on_missing_assets,
  386. layout_target=layout_target,
  387. override_pak_folder=override_pak_folder,
  388. copy=copy)
  389. def sync_layout_loose(target_platform, project_path, asset_type, warning_on_missing_assets, layout_target,
  390. override_pak_folder, copy):
  391. sync_layout_non_vfs(mode=ASSET_MODE_LOOSE,
  392. target_platform=target_platform,
  393. project_path=project_path,
  394. asset_type=asset_type,
  395. warning_on_missing_assets=warning_on_missing_assets,
  396. layout_target=layout_target,
  397. override_pak_folder=override_pak_folder,
  398. copy=copy)
  399. ASSET_SYNC_MODE_FUNCTION = {
  400. ASSET_MODE_VFS: sync_layout_vfs,
  401. ASSET_MODE_PAK: sync_layout_pak,
  402. ASSET_MODE_LOOSE: sync_layout_loose
  403. }
  404. def main(args):
  405. parser = argparse.ArgumentParser(description="Synchronize a project's assets to a layout folder")
  406. parser.add_argument('--project-path',
  407. help='The project path whose assets we will sync.',
  408. required=True)
  409. parser.add_argument('-p', '--platform',
  410. help='Target platform for the layout.',
  411. required=True)
  412. parser.add_argument('-a', '--asset-type',
  413. help='The asset type to use for this deployment',
  414. default='pc')
  415. parser.add_argument('--debug',
  416. action='store_true',
  417. help='Enable debug logs.')
  418. parser.add_argument('--warn-on-missing-assets',
  419. action='store_true',
  420. help='If the project does not have any built assets, warn rather than return an error')
  421. parser.add_argument('-m', '--mode',
  422. type=str,
  423. choices=ALL_ASSET_MODES,
  424. default=ASSET_MODE_LOOSE,
  425. help='Asset Mode (vfs|pak|loose)')
  426. parser.add_argument('-l', '--layout-root',
  427. help='The layout root to where the sync of the assets will occur',
  428. required=True)
  429. parser.add_argument('--create-layout-root',
  430. action='store_true',
  431. help='If the layout root doesnt exist, create it')
  432. parser.add_argument('--override-pak-folder',
  433. default='',
  434. help='(optional) If provided, use this path as the path to the pak folder when creating layouts '
  435. 'in PAK mode. Otherwise, use the {project_path}/pak/${project}_${asset_type}_pak as the source pak folder')
  436. parser.add_argument('--build-config',
  437. default='',
  438. help='(optional) If provided, will adjust the asset mode if the provided build-config is "release"')
  439. parser.add_argument('-c', '--copy',
  440. action='store_true',
  441. help='Copy the files instead of symlinking.')
  442. parser.add_argument('--verify',
  443. action='store_true',
  444. help='Option to perform a verification and report warnings against bootstrap and system configs based on the asset mode and type.')
  445. parser.add_argument('--fail-on-warning',
  446. action='store_true',
  447. help='Option to perform a verification of the layout against the bootstrap and system configs.')
  448. parsed_args = parser.parse_args(args)
  449. # Prepare the logging
  450. logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG if parsed_args.debug else logging.INFO)
  451. # Validate the asset mode
  452. input_asset_mode = parsed_args.mode.upper()
  453. if input_asset_mode not in ALL_ASSET_MODES:
  454. raise common.LmbrCmdError("Invalid asset mode '{}'. Must be one of : '{}'.".format(input_asset_mode, ','.join(ALL_ASSET_MODES)),
  455. common.ERROR_CODE_INVALID_PARAMETER)
  456. # Check if the build config is set, if so, check if its release
  457. build_config = parsed_args.build_config.upper()
  458. if build_config in PAK_ONLY_BUILD_CONFIGS:
  459. input_asset_mode = ASSET_MODE_PAK
  460. logging.info("Starting (%s) Asset Synchronization in %s mode and project %s", parsed_args.asset_type, input_asset_mode, parsed_args.project_path)
  461. start_time = timeit.default_timer()
  462. ASSET_SYNC_MODE_FUNCTION[input_asset_mode](target_platform=parsed_args.platform,
  463. project_path=parsed_args.project_path,
  464. asset_type=parsed_args.asset_type,
  465. warning_on_missing_assets=parsed_args.warn_on_missing_assets,
  466. layout_target=os.path.normpath(parsed_args.layout_root),
  467. override_pak_folder=parsed_args.override_pak_folder,
  468. copy=parsed_args.copy)
  469. duration = timeit.default_timer() - start_time
  470. logging.info("Asset Synchronization complete {:.2f} seconds".format(duration))
  471. # Remove broken symlinks/junctions to the layout folder
  472. if os.path.isdir(parsed_args.layout_root) and not os.path.exists(parsed_args.layout_root):
  473. remove_link(parsed_args.layout_root)
  474. if not os.path.isdir(parsed_args.layout_root):
  475. # If the layout target doesnt exist, check if we want to create it
  476. if parsed_args.create_layout_root:
  477. try:
  478. os.makedirs(parsed_args.layout_root, exist_ok=True)
  479. except OSError as e:
  480. raise common.LmbrCmdError("Unable to create layout folder '{}': {}".format(e,
  481. parsed_args.layout_root),
  482. common.ERROR_CODE_ERROR_DIRECTORY)
  483. else:
  484. raise common.LmbrCmdError("Invalid layout folder (--layout-root): '{}'".format(parsed_args.layout_root),
  485. common.ERROR_CODE_ERROR_DIRECTORY)
  486. if parsed_args.verify:
  487. warnings = verify_layout(layout_dir=os.path.normpath(parsed_args.layout_root),
  488. platform_name=parsed_args.platform,
  489. project_path=parsed_args.project_path,
  490. asset_mode=input_asset_mode,
  491. asset_type=parsed_args.asset_type)
  492. if warnings > 0:
  493. if parsed_args.fail_on_warning:
  494. raise common.LmbrCmdError(f"Layout verification failed: {warnings} warnings.")
  495. logging.warning("%d layout warnings", warnings)
  496. if __name__ == '__main__':
  497. try:
  498. main(sys.argv[1:])
  499. exit(0)
  500. except common.LmbrCmdError as err:
  501. print(str(err), file=sys.stderr)
  502. exit(err.code)