# # 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('6.5') GRADLE_MAX_VERSION = LooseVersion('7.0.0') 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.20.0') CMAKE_VERSION_REGEX = re.compile(r'cmake version (\d+.\d+.?\d*)') CMAKE_EXECUTABLE = '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' 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', 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-platform' ANDROID_SDK_PREFERRED_TOOL_VER = '--android-sdk-build-tool-version' ANDROID_NATIVE_API_LEVEL = '--android-native-api-level' MIN_ANDROID_SDK_PLATFORM = 28 # The minimum platform/api level that is supported for the SDK Platform MIN_NATIVE_API_LEVEL = 24 # The minimum Native API level that is supported for the NDK ANDROID_NDK_PLATFORM_ARGUMENT_NAME = '--android-ndk-version' ANDROID_GRADLE_PLUGIN_ARGUMENT_NAME = '--gradle-plugin-version' ANDROID_GRADLE_MIN_PLUGIN_VERSION = LooseVersion("4.2.0") # 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 = 'android' 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") # Required Arguments parser.add_argument('--engine-root', help='The path to the engine root. Defaults to the current working directory.', default=os.getcwd()) parser.add_argument('--build-dir', help='The build dir path. It will be concatenated to the project-path using the rules of os.path.join', type=pathlib.Path, required=True) parser.add_argument('--third-party-path', help='The path to the 3rd Party root directory', required=True) parser.add_argument(ANDROID_SDK_ARGUMENT_NAME, help='The path to the android SDK', required=True) parser.add_argument('-g', '--project-path', help='The project path to generate an android project', required=True) parser.add_argument(ANDROID_SDK_PLATFORM_ARGUMENT_NAME, help=f'The android SDK platform number version to use for the APK. (Minimum {MIN_ANDROID_SDK_PLATFORM})', type=int, default=-1) parser.add_argument(ANDROID_NATIVE_API_LEVEL, 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_ANDROID_SDK_PLATFORM})', type=int, default=-1) # Override arguments parser.add_argument(ANDROID_SDK_PREFERRED_TOOL_VER, help='The android SDK build tools version.', required=False) parser.add_argument(ANDROID_NDK_PLATFORM_ARGUMENT_NAME, help='The android NDK version', required=False) 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(ANDROID_GRADLE_PLUGIN_ARGUMENT_NAME, help=f'The version of the android gradle plugin to use. Defaults to the minimum version ({ANDROID_GRADLE_MIN_PLUGIN_VERSION})', default=str(ANDROID_GRADLE_MIN_PLUGIN_VERSION)) 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) # 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=f'Asset Mode (vfs|pak|loose) to use when including assets into the APK. (Defaults to {ASSET_MODE_LOOSE})') parser.add_argument(ASSET_TYPE_ARGUMENT_NAME, default=DEFAULT_ASSET_TYPE, help=f'Asset Type to use when including assets into the APK. (Defaults to {DEFAULT_ASSET_TYPE})') 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.') parser.add_argument('--overwrite-existing', action='store_true', help='Option to overwrite existing scripts in the target build folder if they exist already.') 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)) # Get the android sdk platform version to use from the arguments, but also handle the deprecated argument name android_sdk_platform_version = parsed_args.get_argument(ANDROID_SDK_PLATFORM_ARGUMENT_NAME) # Get the gradle plugin details and validate against the current environment android_gradle_plugin_version = parsed_args.get_argument(ANDROID_GRADLE_PLUGIN_ARGUMENT_NAME) android_gradle_plugin = android_support.AndroidGradlePluginInfo(android_gradle_plugin_version) logging.info(f"Generating Android Gradle Plugin version {android_gradle_plugin_version} based project") if gradle_version < android_gradle_plugin.min_gradle_version: raise common.LmbrCmdError(f"The current version of gradle ({gradle_version}) does not satisfy the minimum version " f"({android_gradle_plugin.min_gradle_version}) needed for the android gradle plugin " f"({android_gradle_plugin_version}). Please upgrade your gradle.") if cmake_version < android_gradle_plugin.min_cmake_version: raise common.LmbrCmdError(f"The current version of cmake ({cmake_version}) does not satisfy the minimum version " f"({android_gradle_plugin.min_cmake_version}) needed for the android gradle plugin " f"({android_gradle_plugin_version}). Please upgrade your cmake.") if android_gradle_plugin.max_cmake_version and cmake_version > android_gradle_plugin.max_cmake_version: raise common.LmbrCmdError(f"The current version of cmake ({cmake_version}) exceeds the maximum version " f"({android_gradle_plugin.max_cmake_version}) of the android gradle plugin " f"({android_gradle_plugin_version}).") # Use the SDK Resolver to make sure the build tools and ndk android_sdk = android_support.AndroidSDKResolver(android_sdk_path=parsed_args.get_argument(ANDROID_SDK_ARGUMENT_NAME)) # If no SDK platform is provided, check for any installed one if android_sdk_platform_version < 0: android_sdk_platform_version = MIN_ANDROID_SDK_PLATFORM installed_android_sdk_platforms = android_sdk.is_package_installed('platforms;*') if installed_android_sdk_platforms: # If there are installed platforms, check the most recent one latest_platform_version = -1 for installed_android_sdk_platform in installed_android_sdk_platforms: platform_number_match = re.match(r'platforms;android-([0-9]*)', installed_android_sdk_platform.path) if not platform_number_match: continue check_platform_version = int(platform_number_match.group(1)) if check_platform_version > latest_platform_version: latest_platform_version = check_platform_version if latest_platform_version >= MIN_ANDROID_SDK_PLATFORM: android_sdk_platform_version = latest_platform_version else: if android_sdk_platform_version < MIN_ANDROID_SDK_PLATFORM: 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}.") # Get the android native api level from the arguments. Default to the sdk platform version if not provided android_native_api_level = parsed_args.get_argument(ANDROID_NATIVE_API_LEVEL) if android_native_api_level < 0: android_native_api_level = android_sdk_platform_version else: if android_native_api_level < MIN_NATIVE_API_LEVEL: 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}.") # Check and make sure that the requested sdk platform exists, download if necessary platform_package_name = f"platforms;android-{android_sdk_platform_version}" android_sdk.install_package(package_install_path=platform_package_name, package_description=f'Android SDK Platform {android_sdk_platform_version}') # Make sure we have the extra android packages "market_apk_expansion" and "market_licensing" which is needed by the APK android_sdk.install_package(package_install_path='extras;google;market_apk_expansion', package_description='Google APK Expansion Library') android_sdk.install_package(package_install_path='extras;google;market_licensing', package_description='Google Play Licensing Library') # Install either the requested SDK build tools or the default one for the android gradle plugin version build_tools_version = parsed_args.get_argument(ANDROID_SDK_PREFERRED_TOOL_VER) or android_gradle_plugin.default_sdk_build_tools_version build_tools_package_name = f'build-tools;{build_tools_version}' build_tools_package = android_sdk.install_package(package_install_path=build_tools_package_name, package_description='Android SDK Build Tools') # Install either the requested NDK version or the default one for the android gradle plugin version android_ndk_version = parsed_args.get_argument(ANDROID_NDK_PLATFORM_ARGUMENT_NAME) or android_gradle_plugin.default_ndk_version android_ndk_package_name = f'ndk;{android_ndk_version}' android_ndk_package = android_sdk.install_package(package_install_path=android_ndk_package_name, package_description='Android NDK') # Verify the engine root path and project path verified_project_path, verified_engine_root = common.verify_project_and_engine_root(project_root=parsed_args.project_path, engine_root=parsed_args.engine_root) is_test_project = parsed_args.unit_test # Verify the 3rd Party Root Path third_party_path = pathlib.Path(parsed_args.third_party_path) if not third_party_path.is_dir(): raise common.LmbrCmdError(f"Invalid --third-party-path '{parsed_args.third_party_path}'.", common.ERROR_CODE_INVALID_PARAMETER) third_party_path = third_party_path.parent build_dir = parsed_args.build_dir signing_config = build_optional_signing_profile(store_file=parsed_args.get_argument(SIGNING_PROFILE_STORE_FILE_ARGUMENT_NAME), store_password=parsed_args.get_argument(SIGNING_PROFILE_STORE_PASSWORD_ARGUMENT_NAME), key_alias=parsed_args.get_argument(SIGNING_PROFILE_KEY_ALIAS_ARGUMENT_NAME), key_password=parsed_args.get_argument(SIGNING_PROFILE_KEY_PASSWORD_ARGUMENT_NAME)) logging.debug("Engine Root : %s", str(verified_engine_root.resolve())) logging.debug("Build Path : %s", str(build_dir.resolve())) # Prepare the generator and execute generator = android_support.AndroidProjectGenerator(engine_root=verified_engine_root, build_dir=build_dir, android_sdk_path=android_sdk.android_sdk_path, build_tool=build_tools_package, android_sdk_platform=android_sdk_platform_version, android_native_api_level=android_native_api_level, android_ndk=android_ndk_package, project_path=verified_project_path, third_party_path=third_party_path, cmake_version=cmake_version, override_cmake_path=override_cmake_path, override_gradle_path=override_gradle_path, gradle_version=gradle_version, gradle_plugin_version=android_gradle_plugin_version, override_ninja_path=override_ninja_path, 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, overwrite_existing=parsed_args.overwrite_existing) 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)