generate_android_project.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  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 logging
  10. import os
  11. import pathlib
  12. import platform
  13. import re
  14. import sys
  15. from packaging.version import Version
  16. ROOT_DEV_PATH = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', '..', '..', '..'))
  17. if ROOT_DEV_PATH not in sys.path:
  18. sys.path.append(ROOT_DEV_PATH)
  19. from cmake.Tools import common
  20. from cmake.Tools.Platform.Android import android_support
  21. O3DE_SCRIPTS_PATH = os.path.join(ROOT_DEV_PATH, 'scripts', 'o3de')
  22. if O3DE_SCRIPTS_PATH not in sys.path:
  23. sys.path.append(O3DE_SCRIPTS_PATH)
  24. from o3de import manifest
  25. GRADLE_ARGUMENT_NAME = '--gradle-install-path'
  26. GRADLE_MIN_VERSION = Version('6.5')
  27. GRADLE_MAX_VERSION = Version('7.5.1')
  28. GRADLE_VERSION_REGEX = re.compile(r"Gradle\s(\d+.\d+.?\d*)")
  29. GRADLE_EXECUTABLE = 'gradle.bat' if platform.system() == 'Windows' else 'gradle'
  30. def verify_gradle(override_gradle_path=None):
  31. """
  32. Verify the installed gradle requirement.
  33. """
  34. return common.verify_tool(override_tool_path=override_gradle_path,
  35. tool_name='gradle',
  36. tool_filename=GRADLE_EXECUTABLE,
  37. argument_name=GRADLE_ARGUMENT_NAME,
  38. tool_version_argument='-v',
  39. tool_version_regex=GRADLE_VERSION_REGEX,
  40. min_version=GRADLE_MIN_VERSION,
  41. max_version=GRADLE_MAX_VERSION)
  42. CMAKE_ARGUMENT_NAME = '--cmake-install-path'
  43. CMAKE_MIN_VERSION = Version('3.20.0')
  44. CMAKE_VERSION_REGEX = re.compile(r'cmake version (\d+.\d+.?\d*)')
  45. CMAKE_EXECUTABLE = 'cmake'
  46. def verify_cmake(override_cmake_path=None):
  47. """
  48. Verify the installed cmake requirement.
  49. """
  50. return common.verify_tool(override_tool_path=override_cmake_path,
  51. tool_name='cmake',
  52. tool_filename=CMAKE_EXECUTABLE,
  53. argument_name=CMAKE_ARGUMENT_NAME,
  54. tool_version_argument='--version',
  55. tool_version_regex=CMAKE_VERSION_REGEX,
  56. min_version=CMAKE_MIN_VERSION,
  57. max_version=None)
  58. NINJA_ARGUMENT_NAME = '--ninja-install-path'
  59. NINJA_VERSION_REGEX = re.compile(r'(\d+.\d+.?\d*)')
  60. NINJA_EXECUTABLE = 'ninja'
  61. def verify_ninja(override_ninja_path=None):
  62. """
  63. Verify the installed ninja requirement.
  64. """
  65. return common.verify_tool(override_tool_path=override_ninja_path,
  66. tool_name='ninja',
  67. tool_filename='ninja',
  68. argument_name=NINJA_ARGUMENT_NAME,
  69. tool_version_argument='--version',
  70. tool_version_regex=NINJA_VERSION_REGEX,
  71. min_version=None,
  72. max_version=None)
  73. SIGNING_PROFILE_STORE_FILE_ARGUMENT_NAME = "--signconfig-store-file"
  74. SIGNING_PROFILE_STORE_PASSWORD_ARGUMENT_NAME = "--signconfig-store-password"
  75. SIGNING_PROFILE_KEY_ALIAS_ARGUMENT_NAME = "--signconfig-key-alias"
  76. SIGNING_PROFILE_KEY_PASSWORD_ARGUMENT_NAME = "--signconfig-key-password"
  77. def build_optional_signing_profile(store_file, store_password, key_alias, key_password):
  78. # If none of the arguments are set, then return None and skip the signing config generation
  79. if not any([store_file, store_password, key_alias, key_password]):
  80. return
  81. return android_support.AndroidSigningConfig(store_file=store_file,
  82. store_password=store_password,
  83. key_alias=key_alias,
  84. key_password=key_password)
  85. ANDROID_SDK_ARGUMENT_NAME = '--android-sdk-path'
  86. ANDROID_SDK_PLATFORM_ARGUMENT_NAME = '--android-sdk-platform'
  87. ANDROID_SDK_PREFERRED_TOOL_VER = '--android-sdk-build-tool-version'
  88. ANDROID_SDK_COMMAND_LINE_TOOLS_VER = '--android-sdk-command-line-tools-version'
  89. ANDROID_NATIVE_API_LEVEL = '--android-native-api-level'
  90. MIN_ANDROID_SDK_PLATFORM = 28 # The minimum platform/api level that is supported for the SDK Platform
  91. MIN_NATIVE_API_LEVEL = 24 # The minimum Native API level that is supported for the NDK
  92. ANDROID_NDK_PLATFORM_ARGUMENT_NAME = '--android-ndk-version'
  93. ANDROID_GRADLE_PLUGIN_ARGUMENT_NAME = '--gradle-plugin-version'
  94. ANDROID_GRADLE_MIN_PLUGIN_VERSION = Version("4.2.2")
  95. # Constants for asset-related options for APK generation
  96. INCLUDE_APK_ASSETS_ARGUMENT_NAME = "--include-apk-assets"
  97. ASSET_MODE_ARGUMENT_NAME = "--asset-mode"
  98. ASSET_MODE_PAK = 'PAK'
  99. ASSET_MODE_LOOSE = 'LOOSE'
  100. ASSET_MODE_VFS = 'VFS'
  101. ALL_ASSET_MODES = [ASSET_MODE_PAK, ASSET_MODE_LOOSE, ASSET_MODE_VFS]
  102. ASSET_TYPE_ARGUMENT_NAME = '--asset-type'
  103. DEFAULT_ASSET_TYPE = 'android'
  104. manifest_json = manifest.load_o3de_manifest()
  105. DEFAULT_3RD_PARTY_PATH = pathlib.Path(manifest_json.get('default_third_party_folder', manifest.get_o3de_third_party_folder()))
  106. def wrap_parsed_args(parsed_args):
  107. """
  108. Function to add a method to the parsed argument object to transform a long-form argument name to and get the
  109. parsed values based on the input long form.
  110. This will allow us to read an argument like '--foo-bar=Orange' by using the built-in method rather than looking for
  111. the argparsed transformed attribute 'foo_bar'.
  112. :param parsed_args: The parsed args object to wrap
  113. """
  114. def parse_argument_attr(argument):
  115. argument_attr = argument[2:].replace('-', '_')
  116. return getattr(parsed_args, argument_attr)
  117. parsed_args.get_argument = parse_argument_attr
  118. def main(args):
  119. """
  120. Perform the main argument processing and execution of the project generator
  121. :param args: The arguments to process
  122. """
  123. parser = argparse.ArgumentParser(description="Prepare the android studio subfolder")
  124. # Required Arguments
  125. parser.add_argument('--engine-root',
  126. help='The path to the engine root. Defaults to the current working directory.',
  127. default=os.getcwd())
  128. parser.add_argument('--build-dir',
  129. help='The build dir path. It will be concatenated to the project-path using the rules of os.path.join',
  130. type=pathlib.Path,
  131. required=True)
  132. parser.add_argument('--third-party-path',
  133. help=f'The path to the 3rd Party root directory (defaults to {DEFAULT_3RD_PARTY_PATH})',
  134. type=pathlib.Path,
  135. default=DEFAULT_3RD_PARTY_PATH)
  136. parser.add_argument(ANDROID_SDK_ARGUMENT_NAME,
  137. help='The path to the android SDK',
  138. required=True)
  139. parser.add_argument('-g', '--project-path',
  140. help='The project path to generate an android project',
  141. required=True)
  142. parser.add_argument(ANDROID_SDK_PLATFORM_ARGUMENT_NAME,
  143. help=f'The android SDK platform number version to use for the APK. (Minimum {MIN_ANDROID_SDK_PLATFORM})',
  144. type=int,
  145. default=-1)
  146. parser.add_argument(ANDROID_NATIVE_API_LEVEL,
  147. help=f'The android native API level to use for the APK. If not set, this will default to the android SDK platform. (Minimum {MIN_NATIVE_API_LEVEL})',
  148. type=int,
  149. default=-1)
  150. # Override arguments
  151. parser.add_argument(ANDROID_SDK_COMMAND_LINE_TOOLS_VER,
  152. default='latest',
  153. help='The android SDK command line tools version.',
  154. required=False)
  155. parser.add_argument(ANDROID_SDK_PREFERRED_TOOL_VER,
  156. help='The android SDK build tools version.',
  157. required=False)
  158. parser.add_argument(ANDROID_NDK_PLATFORM_ARGUMENT_NAME,
  159. help='The android NDK version',
  160. required=False)
  161. parser.add_argument(GRADLE_ARGUMENT_NAME,
  162. help=f'The path to installed gradle. The version of gradle must fall in between {str(GRADLE_MIN_VERSION)} and {str(GRADLE_MAX_VERSION)}.',
  163. default=None,
  164. required=False)
  165. parser.add_argument(ANDROID_GRADLE_PLUGIN_ARGUMENT_NAME,
  166. help=f'The version of the android gradle plugin to use. Defaults to the minimum version ({ANDROID_GRADLE_MIN_PLUGIN_VERSION})',
  167. default=str(ANDROID_GRADLE_MIN_PLUGIN_VERSION))
  168. parser.add_argument(CMAKE_ARGUMENT_NAME,
  169. 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)}.',
  170. default=None,
  171. required=False)
  172. parser.add_argument(NINJA_ARGUMENT_NAME,
  173. help='The path to the ninja build tool if not installed on the system path.',
  174. default=None,
  175. required=False)
  176. parser.add_argument('--native-build-path',
  177. help='Custom path to place native build artifacts.',
  178. default=None,
  179. required=False)
  180. parser.add_argument('--vulkan-validation-path',
  181. help='Override path to where the Vulkan Validation Layers libraries are. Required for use with NDK r23+',
  182. default=None,
  183. required=False)
  184. parser.add_argument('--extra-cmake-configure-args',
  185. help='Extra arguments to supply to the cmake configure step',
  186. nargs='*')
  187. # Asset Options
  188. parser.add_argument(INCLUDE_APK_ASSETS_ARGUMENT_NAME,
  189. action='store_true',
  190. help='Option to include the game assets when building the APK. If this option is set, you must have the android assets built.')
  191. parser.add_argument(ASSET_MODE_ARGUMENT_NAME,
  192. choices=ALL_ASSET_MODES,
  193. default=ASSET_MODE_LOOSE,
  194. help=f'Asset Mode (vfs|pak|loose) to use when including assets into the APK. (Defaults to {ASSET_MODE_LOOSE})')
  195. parser.add_argument(ASSET_TYPE_ARGUMENT_NAME,
  196. default=DEFAULT_ASSET_TYPE,
  197. help=f'Asset Type to use when including assets into the APK. (Defaults to {DEFAULT_ASSET_TYPE})')
  198. parser.add_argument('--debug',
  199. action='store_true',
  200. help='Enable debug logs.')
  201. # Signing Config options
  202. parser.add_argument(SIGNING_PROFILE_STORE_FILE_ARGUMENT_NAME,
  203. default=None,
  204. help='(Optional) If specified, create a signing profile based on this supplied android jks keystore file.')
  205. parser.add_argument(SIGNING_PROFILE_STORE_PASSWORD_ARGUMENT_NAME,
  206. default=None,
  207. help='If an android jks keystore file is specified, this is the store password for the keystore.')
  208. parser.add_argument(SIGNING_PROFILE_KEY_ALIAS_ARGUMENT_NAME,
  209. default=None,
  210. help='If an android jks keystore file is specified, this is the alias of the signing key in the keystore.')
  211. parser.add_argument(SIGNING_PROFILE_KEY_PASSWORD_ARGUMENT_NAME,
  212. default=None,
  213. help='If an android jks keystore file is specified, this is the password of the signing key in the keystore.')
  214. parser.add_argument('--unit-test',
  215. action='store_true',
  216. help='Generate a unit test APK instead of a game APK.')
  217. parser.add_argument('--overwrite-existing',
  218. action='store_true',
  219. help='Option to overwrite existing scripts in the target build folder if they exist already.')
  220. parser.add_argument('--enable-unity-build',
  221. action='store_true',
  222. help='Enable unity build')
  223. parser.add_argument('--oculus-project',
  224. action='store_true',
  225. help='Generate android project for Oculus app.')
  226. parsed_args = parser.parse_args(args)
  227. wrap_parsed_args(parsed_args)
  228. # Prepare the logging
  229. logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.DEBUG if parsed_args.debug else logging.INFO)
  230. # Verify the gradle requirements
  231. gradle_version, override_gradle_path = verify_gradle(override_gradle_path=parsed_args.get_argument(GRADLE_ARGUMENT_NAME))
  232. logging.info("Detected Gradle version %s", str(gradle_version))
  233. # Verify the cmake requirements
  234. cmake_version, override_cmake_path = verify_cmake(override_cmake_path=parsed_args.get_argument(CMAKE_ARGUMENT_NAME))
  235. logging.info("Detected CMake version %s", str(cmake_version))
  236. # Verify the ninja requirements
  237. ninja_version, override_ninja_path = verify_ninja(override_ninja_path=parsed_args.get_argument(NINJA_ARGUMENT_NAME))
  238. logging.info("Detected Ninja version %s", str(ninja_version))
  239. # Get the android sdk platform version to use from the arguments, but also handle the deprecated argument name
  240. android_sdk_platform_version = parsed_args.get_argument(ANDROID_SDK_PLATFORM_ARGUMENT_NAME)
  241. # Get the gradle plugin details and validate against the current environment
  242. android_gradle_plugin_version = parsed_args.get_argument(ANDROID_GRADLE_PLUGIN_ARGUMENT_NAME)
  243. android_gradle_plugin = android_support.AndroidGradlePluginInfo(android_gradle_plugin_version)
  244. logging.info(f"Generating Android Gradle Plugin version {android_gradle_plugin_version} based project")
  245. if gradle_version < android_gradle_plugin.min_gradle_version:
  246. raise common.LmbrCmdError(f"The current version of gradle ({gradle_version}) does not satisfy the minimum version "
  247. f"({android_gradle_plugin.min_gradle_version}) needed for the android gradle plugin "
  248. f"({android_gradle_plugin_version}). Please upgrade your gradle.")
  249. if cmake_version < android_gradle_plugin.min_cmake_version:
  250. raise common.LmbrCmdError(f"The current version of cmake ({cmake_version}) does not satisfy the minimum version "
  251. f"({android_gradle_plugin.min_cmake_version}) needed for the android gradle plugin "
  252. f"({android_gradle_plugin_version}). Please upgrade your cmake.")
  253. if android_gradle_plugin.max_cmake_version and cmake_version > android_gradle_plugin.max_cmake_version:
  254. raise common.LmbrCmdError(f"The current version of cmake ({cmake_version}) exceeds the maximum version "
  255. f"({android_gradle_plugin.max_cmake_version}) of the android gradle plugin "
  256. f"({android_gradle_plugin_version}).")
  257. # Use the SDK Resolver to make sure the build tools and ndk
  258. android_sdk = android_support.AndroidSDKResolver(android_sdk_path=parsed_args.get_argument(ANDROID_SDK_ARGUMENT_NAME),
  259. command_line_tools_version=parsed_args.get_argument(ANDROID_SDK_COMMAND_LINE_TOOLS_VER))
  260. # If no SDK platform is provided, check for any installed one
  261. if android_sdk_platform_version < 0:
  262. android_sdk_platform_version = MIN_ANDROID_SDK_PLATFORM
  263. installed_android_sdk_platforms = android_sdk.is_package_installed('platforms;*')
  264. if installed_android_sdk_platforms:
  265. # If there are installed platforms, check the most recent one
  266. latest_platform_version = -1
  267. for installed_android_sdk_platform in installed_android_sdk_platforms:
  268. platform_number_match = re.match(r'platforms;android-([0-9]*)', installed_android_sdk_platform.path)
  269. if not platform_number_match:
  270. continue
  271. check_platform_version = int(platform_number_match.group(1))
  272. if check_platform_version > latest_platform_version:
  273. latest_platform_version = check_platform_version
  274. if latest_platform_version >= MIN_ANDROID_SDK_PLATFORM:
  275. android_sdk_platform_version = latest_platform_version
  276. else:
  277. if android_sdk_platform_version < MIN_ANDROID_SDK_PLATFORM:
  278. raise common.LmbrCmdError(f"Invalid argument for {ANDROID_SDK_PLATFORM_ARGUMENT_NAME} ({android_sdk_platform_version}). Must be greater than the minimum value supported {MIN_ANDROID_SDK_PLATFORM}.")
  279. # Get the android native api level from the arguments. Default to the sdk platform version if not provided
  280. android_native_api_level = parsed_args.get_argument(ANDROID_NATIVE_API_LEVEL)
  281. if android_native_api_level < 0:
  282. android_native_api_level = android_sdk_platform_version
  283. else:
  284. if android_native_api_level < MIN_NATIVE_API_LEVEL:
  285. raise common.LmbrCmdError(f"Invalid argument for {ANDROID_NATIVE_API_LEVEL} ({android_native_api_level}). Must be greater than the minimum value supported {MIN_NATIVE_API_LEVEL}.")
  286. # Check and make sure that the requested sdk platform exists, download if necessary
  287. platform_package_name = f"platforms;android-{android_sdk_platform_version}"
  288. android_sdk.install_package(package_install_path=platform_package_name,
  289. package_description=f'Android SDK Platform {android_sdk_platform_version}')
  290. # Make sure we have the extra android packages "market_apk_expansion" and "market_licensing" which is needed by the APK
  291. android_sdk.install_package(package_install_path='extras;google;market_apk_expansion',
  292. package_description='Google APK Expansion Library')
  293. android_sdk.install_package(package_install_path='extras;google;market_licensing',
  294. package_description='Google Play Licensing Library')
  295. # Install either the requested SDK build tools or the default one for the android gradle plugin version
  296. build_tools_version = parsed_args.get_argument(ANDROID_SDK_PREFERRED_TOOL_VER) or android_gradle_plugin.default_sdk_build_tools_version
  297. build_tools_package_name = f'build-tools;{build_tools_version}'
  298. build_tools_package = android_sdk.install_package(package_install_path=build_tools_package_name,
  299. package_description='Android SDK Build Tools')
  300. # Install either the requested NDK version or the default one for the android gradle plugin version
  301. android_ndk_version = parsed_args.get_argument(ANDROID_NDK_PLATFORM_ARGUMENT_NAME) or android_gradle_plugin.default_ndk_version
  302. android_ndk_package_name = f'ndk;{android_ndk_version}'
  303. android_ndk_package = android_sdk.install_package(package_install_path=android_ndk_package_name,
  304. package_description='Android NDK')
  305. # Verify the engine root path and project path
  306. verified_project_path, verified_engine_root = common.verify_project_and_engine_root(project_root=parsed_args.project_path,
  307. engine_root=parsed_args.engine_root)
  308. is_test_project = parsed_args.unit_test
  309. # Verify the 3rd Party Root Path
  310. third_party_path = pathlib.Path(parsed_args.third_party_path)
  311. if not third_party_path.is_dir():
  312. raise common.LmbrCmdError(f"Invalid --third-party-path '{parsed_args.third_party_path}'.",
  313. common.ERROR_CODE_INVALID_PARAMETER)
  314. build_dir = parsed_args.build_dir
  315. signing_config = build_optional_signing_profile(store_file=parsed_args.get_argument(SIGNING_PROFILE_STORE_FILE_ARGUMENT_NAME),
  316. store_password=parsed_args.get_argument(SIGNING_PROFILE_STORE_PASSWORD_ARGUMENT_NAME),
  317. key_alias=parsed_args.get_argument(SIGNING_PROFILE_KEY_ALIAS_ARGUMENT_NAME),
  318. key_password=parsed_args.get_argument(SIGNING_PROFILE_KEY_PASSWORD_ARGUMENT_NAME))
  319. logging.debug("Engine Root : %s", str(verified_engine_root.resolve()))
  320. logging.debug("Build Path : %s", str(build_dir.resolve()))
  321. # Prepare the generator and execute
  322. generator = android_support.AndroidProjectGenerator(engine_root=verified_engine_root,
  323. build_dir=build_dir,
  324. android_sdk_path=android_sdk.android_sdk_path,
  325. build_tool=build_tools_package,
  326. android_sdk_platform=android_sdk_platform_version,
  327. android_native_api_level=android_native_api_level,
  328. android_ndk=android_ndk_package,
  329. project_path=verified_project_path,
  330. third_party_path=third_party_path,
  331. cmake_version=cmake_version,
  332. override_cmake_path=override_cmake_path,
  333. override_gradle_path=override_gradle_path,
  334. gradle_version=gradle_version,
  335. gradle_plugin_version=android_gradle_plugin_version,
  336. override_ninja_path=override_ninja_path,
  337. include_assets_in_apk=parsed_args.include_apk_assets,
  338. asset_mode=parsed_args.get_argument(ASSET_MODE_ARGUMENT_NAME),
  339. asset_type=parsed_args.get_argument(ASSET_TYPE_ARGUMENT_NAME),
  340. signing_config=signing_config,
  341. is_test_project=is_test_project,
  342. overwrite_existing=parsed_args.overwrite_existing,
  343. unity_build_enabled=parsed_args.enable_unity_build,
  344. oculus_project=parsed_args.oculus_project,
  345. native_build_path=parsed_args.native_build_path,
  346. vulkan_validation_path=parsed_args.vulkan_validation_path,
  347. extra_cmake_configure_args=parsed_args.extra_cmake_configure_args)
  348. generator.execute()
  349. if __name__ == '__main__':
  350. try:
  351. main(sys.argv[1:])
  352. exit(0)
  353. except common.LmbrCmdError as err:
  354. print(str(err), file=sys.stderr)
  355. exit(err.code)