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.
1669 lines
82 KiB
Python
1669 lines
82 KiB
Python
#
|
|
# 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 imghdr
|
|
import configparser
|
|
import datetime
|
|
import fnmatch
|
|
import logging
|
|
import os
|
|
import json
|
|
import platform
|
|
import re
|
|
import shutil
|
|
import stat
|
|
import string
|
|
import sys
|
|
import subprocess
|
|
import pathlib
|
|
|
|
from distutils.version import LooseVersion
|
|
|
|
# Resolve the common python module
|
|
ROOT_DEV_PATH = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', '..', '..', '..'))
|
|
if ROOT_DEV_PATH not in sys.path:
|
|
sys.path.append(ROOT_DEV_PATH)
|
|
|
|
from cmake.Tools import common
|
|
|
|
|
|
ANDROID_GRADLE_PLUGIN_COMPATIBILITY_MAP = {
|
|
'4.2.0': {'min_gradle_version': '6.7.1',
|
|
'sdk_build': '30.0.2',
|
|
'default_ndk': '21.4.7075529',
|
|
'min_cmake_version': '3.20'}
|
|
}
|
|
|
|
APP_NAME = 'app'
|
|
ANDROID_MANIFEST_FILE = 'AndroidManifest.xml'
|
|
ANDROID_LIBRARIES_JSON_FILE = 'android_libraries.json'
|
|
BUILD_CONFIGURATIONS = ['Debug', 'Profile', 'Release']
|
|
|
|
# We currently only support arm64-v8a
|
|
ANDROID_ARCH = 'arm64-v8a'
|
|
|
|
ANDROID_RESOLUTION_SETTINGS = ('mdpi', 'hdpi', 'xhdpi', 'xxhdpi', 'xxxhdpi')
|
|
|
|
DEFAULT_CONFIG_CHANGES = [
|
|
'keyboard',
|
|
'keyboardHidden',
|
|
'orientation',
|
|
'screenSize',
|
|
'smallestScreenSize',
|
|
'screenLayout',
|
|
'uiMode',
|
|
]
|
|
|
|
TEST_RUNNER_PROJECT = 'AzTestRunner'
|
|
TEST_RUNNER_PACKAGE_NAME = 'com.lumberyard.tests'
|
|
|
|
# Android Orientation Constants
|
|
ORIENTATION_LANDSCAPE = 1 << 0
|
|
ORIENTATION_PORTRAIT = 1 << 1
|
|
ORIENTATION_ALL = (ORIENTATION_LANDSCAPE | ORIENTATION_PORTRAIT)
|
|
|
|
ORIENTATION_FLAG_TO_KEY_MAP = {
|
|
ORIENTATION_LANDSCAPE: 'land',
|
|
ORIENTATION_PORTRAIT: 'port',
|
|
}
|
|
|
|
ORIENTATION_MAPPING = {
|
|
'landscape': ORIENTATION_LANDSCAPE,
|
|
'reverseLandscape': ORIENTATION_LANDSCAPE,
|
|
'sensorLandscape': ORIENTATION_LANDSCAPE,
|
|
'userLandscape': ORIENTATION_LANDSCAPE,
|
|
'portrait': ORIENTATION_PORTRAIT,
|
|
'reversePortrait': ORIENTATION_PORTRAIT,
|
|
'sensorPortrait': ORIENTATION_PORTRAIT,
|
|
'userPortrait': ORIENTATION_PORTRAIT
|
|
}
|
|
|
|
MIPMAP_PATH_PREFIX = 'mipmap'
|
|
|
|
APP_ICON_NAME = 'app_icon.png'
|
|
APP_SPLASH_NAME = 'app_splash.png'
|
|
|
|
PYTHON_SCRIPT = 'python.cmd' if platform.system() == 'Windows' else 'python.sh'
|
|
|
|
ANDROID_LAUNCHER_NAME_PATTERN = "{project_name}.GameLauncher"
|
|
|
|
|
|
class AndroidProjectManifestEnvironment(object):
|
|
"""
|
|
This class manages the environment for the AndroidManifest.xml template file, based on project settings and environments
|
|
that were passed in or calculated from the command line arguments.
|
|
"""
|
|
|
|
def __init__(self, engine_root, project_path, android_sdk_version_number, is_test:bool):
|
|
"""
|
|
Initialize the object with the project specific parameters and values for the game project
|
|
|
|
:param engine_root: The path where the engine is located
|
|
:param project_path: The path were the project is located
|
|
:param android_sdk_version_number: The android SDK platform version
|
|
:param is_test: Indicates if theAzTestRunner application should be run
|
|
"""
|
|
|
|
try:
|
|
if is_test:
|
|
# The AzTestRunner project.json is located under {engine_root}/Code/Tools/AzTestRunner/Platform/Android/android_project.json
|
|
project_properties_path = engine_root / 'Code' / 'Tools' / 'AzTestRunner' / 'Platform' / 'Android' / 'android_project.json'
|
|
assert project_properties_path.is_file(), f'Missing required android settings file {project_properties_path.resolve()}'
|
|
project_properties_content = project_properties_path.read_text(encoding=common.DEFAULT_TEXT_READ_ENCODING,
|
|
errors=common.ENCODING_ERROR_HANDLINGS)
|
|
project_json = json.loads(project_properties_content)
|
|
|
|
android_settings = project_json['android_settings']
|
|
|
|
else:
|
|
# O3DE projects have both a project.json and an android_project.json files (unless its internal)
|
|
project_properties_path = project_path / 'project.json'
|
|
assert project_properties_path.is_file(), f'Missing required project settings file {project_properties_path.resolve()}'
|
|
project_properties_content = project_properties_path.read_text(encoding=common.DEFAULT_TEXT_READ_ENCODING,
|
|
errors=common.ENCODING_ERROR_HANDLINGS)
|
|
project_json = json.loads(project_properties_content)
|
|
|
|
android_project_properties_path = project_path / 'Platform' / 'Android' / 'android_project.json'
|
|
if android_project_properties_path.is_file():
|
|
android_project_properties_content = android_project_properties_path.read_text(encoding=common.DEFAULT_TEXT_READ_ENCODING,
|
|
errors=common.ENCODING_ERROR_HANDLINGS)
|
|
android_project_json = json.loads(android_project_properties_content)
|
|
android_settings = android_project_json['android_settings']
|
|
else:
|
|
android_settings = project_json['android_settings']
|
|
|
|
self.project_path = project_path
|
|
|
|
project_name = project_json['project_name']
|
|
product_name = project_json.get('product_name', project_name)
|
|
package_name = android_settings["package_name"]
|
|
package_path = package_name.replace('.', '/')
|
|
|
|
project_activity = f'{TEST_RUNNER_PROJECT}Activity' if is_test else f'{project_name}Activity'
|
|
|
|
# Multiview options require special processing
|
|
multi_window_options = AndroidProjectManifestEnvironment.process_android_multi_window_options(android_settings)
|
|
|
|
self.internal_dict = {
|
|
'ANDROID_PACKAGE': package_name,
|
|
'ANDROID_PACKAGE_PATH': package_path,
|
|
'ANDROID_VERSION_NUMBER': android_settings["version_number"],
|
|
"ANDROID_VERSION_NAME": android_settings["version_name"],
|
|
"ANDROID_SCREEN_ORIENTATION": android_settings["orientation"],
|
|
'ANDROID_APP_NAME': TEST_RUNNER_PROJECT if is_test else product_name, # external facing name
|
|
'ANDROID_PROJECT_NAME': TEST_RUNNER_PROJECT if is_test else project_name, # internal facing name
|
|
'ANDROID_PROJECT_ACTIVITY': project_activity,
|
|
'ANDROID_LAUNCHER_NAME': TEST_RUNNER_PROJECT if is_test else ANDROID_LAUNCHER_NAME_PATTERN.format(project_name=project_name),
|
|
'ANDROID_CONFIG_CHANGES': multi_window_options['ANDROID_CONFIG_CHANGES'],
|
|
'ANDROID_APP_PUBLIC_KEY': android_settings.get('app_public_key', 'NoKey'),
|
|
'ANDROID_APP_OBFUSCATOR_SALT': android_settings.get('app_obfuscator_salt', ''),
|
|
'ANDROID_USE_MAIN_OBB': android_settings.get('use_main_obb', 'false'),
|
|
'ANDROID_USE_PATCH_OBB': android_settings.get('use_patch_obb', 'false'),
|
|
'ANDROID_ENABLE_KEEP_SCREEN_ON': android_settings.get('enable_keep_screen_on', 'false'),
|
|
'ANDROID_DISABLE_IMMERSIVE_MODE': android_settings.get('disable_immersive_mode', 'false'),
|
|
'ANDROID_TARGET_SDK_VERSION': android_sdk_version_number,
|
|
'ICONS': android_settings.get('icons', None),
|
|
'SPLASH_SCREEN': android_settings.get('splash_screen', None),
|
|
|
|
'ANDROID_MULTI_WINDOW': multi_window_options['ANDROID_MULTI_WINDOW'],
|
|
'ANDROID_MULTI_WINDOW_PROPERTIES': multi_window_options['ANDROID_MULTI_WINDOW_PROPERTIES'],
|
|
|
|
'SAMSUNG_DEX_KEEP_ALIVE': multi_window_options['SAMSUNG_DEX_KEEP_ALIVE'],
|
|
'SAMSUNG_DEX_LAUNCH_WIDTH': multi_window_options['SAMSUNG_DEX_LAUNCH_WIDTH'],
|
|
'SAMSUNG_DEX_LAUNCH_HEIGHT': multi_window_options['SAMSUNG_DEX_LAUNCH_HEIGHT']
|
|
}
|
|
except KeyError as e:
|
|
raise common.LmbrCmdError(f"Missing key from android project settings for project at {project_path}:'{e}' ")
|
|
|
|
def __getitem__(self, item):
|
|
return self.internal_dict.get(item)
|
|
|
|
@staticmethod
|
|
def process_android_multi_window_options(game_project_android_settings):
|
|
"""
|
|
Perform custom processing for game projects that have custom 'multi_window_options' in their project.json definition
|
|
:param game_project_android_settings: The parsed out android settings from the game's project.json
|
|
:return: Dictionary of attributes for any optional multiview option detected from the android settings
|
|
"""
|
|
|
|
def is_number_option_valid(value, name):
|
|
if value:
|
|
if isinstance(value, int):
|
|
return True
|
|
else:
|
|
logging.warning('[WARN] Invalid value for property "%s", expected whole number', name)
|
|
return False
|
|
|
|
def get_int_attribute(settings, key_name):
|
|
settings_value = settings.get(key_name, None)
|
|
if not settings_value:
|
|
return None
|
|
if not isinstance(settings_value, int):
|
|
logging.warning('[WARN] Invalid value for property "%s", expected whole number', key_name)
|
|
return None
|
|
return settings_value
|
|
|
|
multi_window_options = {
|
|
'SAMSUNG_DEX_LAUNCH_WIDTH': '',
|
|
'SAMSUNG_DEX_LAUNCH_HEIGHT': '',
|
|
'SAMSUNG_DEX_KEEP_ALIVE': '',
|
|
'ANDROID_CONFIG_CHANGES': '|'.join(DEFAULT_CONFIG_CHANGES),
|
|
'ANDROID_MULTI_WINDOW_PROPERTIES': '',
|
|
'ANDROID_MULTI_WINDOW': '',
|
|
'ORIENTATION': ORIENTATION_ALL
|
|
}
|
|
|
|
multi_window_settings = game_project_android_settings.get('multi_window_options', None)
|
|
if not multi_window_settings:
|
|
# If there are no multi-window options, then set the orientation to the orientation attribute if set, otherwise use the default 'ALL' orientation
|
|
requested_orientation = game_project_android_settings['orientation']
|
|
multi_window_options['ORIENTATION'] = ORIENTATION_MAPPING.get(requested_orientation, ORIENTATION_ALL)
|
|
return multi_window_options
|
|
|
|
launch_in_fullscreen = False
|
|
|
|
# the Samsung DEX specific values can be added regardless of target API and multi-window support
|
|
samsung_dex_options = multi_window_settings.get('samsung_dex_options', None)
|
|
if samsung_dex_options:
|
|
launch_in_fullscreen = samsung_dex_options.get('launch_in_fullscreen', False)
|
|
|
|
# setting the launch window size in DEX mode since launching in fullscreen is strictly tied
|
|
# to multi-window being enabled
|
|
launch_width = get_int_attribute(samsung_dex_options, 'launch_width')
|
|
launch_height = get_int_attribute(samsung_dex_options, 'launch_height')
|
|
|
|
# both have to be specified otherwise they are ignored
|
|
if launch_width and launch_height:
|
|
multi_window_options['SAMSUNG_DEX_LAUNCH_WIDTH'] = (f'<meta-data '
|
|
f'android:name="com.samsung.android.sdk.multiwindow.dex.launchwidth" '
|
|
f'android:value="{launch_width}"'
|
|
f'/>')
|
|
|
|
multi_window_options['SAMSUNG_DEX_LAUNCH_HEIGHT'] = (f'<meta-data '
|
|
f'android:name="com.samsung.android.sdk.multiwindow.dex.launchheight" '
|
|
f'android:value="{launch_height}"'
|
|
f'/>')
|
|
|
|
keep_alive = samsung_dex_options.get('keep_alive', None)
|
|
if keep_alive in (True, False):
|
|
multi_window_options['SAMSUNG_DEX_KEEP_ALIVE'] = f'<meta-data ' \
|
|
f'android:name="com.samsung.android.keepalive.density" ' \
|
|
f'android:value="{str(keep_alive).lower()}" ' \
|
|
f'/>'
|
|
|
|
multi_window_enabled = multi_window_settings.get('enabled', False)
|
|
|
|
# the option to change the display resolution was added in API 24 as well, these changes are sent as density changes
|
|
multi_window_options['ANDROID_CONFIG_CHANGES'] = '|'.join(DEFAULT_CONFIG_CHANGES + ['density'])
|
|
|
|
# if targeting above the min API level the default value for this attribute is true so we need to explicitly disable it
|
|
multi_window_options['ANDROID_MULTI_WINDOW'] = f'android:resizeableActivity="{str(multi_window_enabled).lower()}"'
|
|
|
|
if not multi_window_enabled:
|
|
return multi_window_options
|
|
|
|
# remove the DEX launch window size if requested to launch in fullscreen mode
|
|
if launch_in_fullscreen:
|
|
multi_window_options['SAMSUNG_DEX_LAUNCH_WIDTH'] = ''
|
|
multi_window_options['SAMSUNG_DEX_LAUNCH_HEIGHT'] = ''
|
|
|
|
default_width = multi_window_settings.get('default_width', None)
|
|
default_height = multi_window_settings.get('default_height', None)
|
|
|
|
min_width = multi_window_settings.get('min_width', None)
|
|
min_height = multi_window_settings.get('min_height', None)
|
|
|
|
gravity = multi_window_settings.get('gravity', None)
|
|
|
|
layout = ''
|
|
if any([default_width, default_height, min_width, min_height, gravity]):
|
|
layout = '<layout '
|
|
|
|
# the default width/height values are respected as launch values in DEX mode so they should
|
|
# be ignored if the intention is to launch in fullscreen when running in DEX mode
|
|
if not launch_in_fullscreen:
|
|
if is_number_option_valid(default_width, 'default_width'):
|
|
layout += f'android:defaultWidth="{default_width}dp" '
|
|
|
|
if is_number_option_valid(default_height, 'default_height'):
|
|
layout += f'android:defaultHeight="{default_height}dp" '
|
|
|
|
if is_number_option_valid(min_height, 'min_height'):
|
|
layout += f'android:minHeight="{min_height}dp" '
|
|
|
|
if is_number_option_valid(min_width, 'min_width'):
|
|
layout += f'android:minWidth="{min_width}dp" '
|
|
|
|
if gravity:
|
|
layout += f'android:gravity="{gravity}" '
|
|
|
|
layout += '/>'
|
|
|
|
multi_window_options['ANDROID_MULTI_WINDOW_PROPERTIES'] = layout
|
|
|
|
return multi_window_options
|
|
|
|
|
|
PLATFORM_SETTINGS_FORMAT = """
|
|
# Auto Generated from last cmake project generation ({generation_timestamp})
|
|
|
|
[settings]
|
|
platform={platform}
|
|
game_projects={project_path}
|
|
asset_deploy_mode={asset_mode}
|
|
asset_deploy_type={asset_type}
|
|
|
|
[android]
|
|
android_sdk_path={android_sdk_path}
|
|
embed_assets_in_apk={embed_assets_in_apk}
|
|
is_unit_test={is_unit_test}
|
|
android_gradle_plugin={android_gradle_plugin_version}
|
|
"""
|
|
|
|
NATIVE_CMAKE_SECTION_ANDROID_FORMAT = """
|
|
externalNativeBuild {{
|
|
cmake {{
|
|
buildStagingDirectory "{native_build_path}"
|
|
version "{cmake_version}"
|
|
path "{absolute_cmakelist_path}"
|
|
}}
|
|
}}
|
|
"""
|
|
|
|
NATIVE_CMAKE_SECTION_DEFAULT_CONFIG_NDK_FORMAT_STR = """
|
|
ndk {{
|
|
abiFilters '{abi}'
|
|
}}
|
|
"""
|
|
|
|
NATIVE_CMAKE_SECTION_BUILD_TYPE_CONFIG_FORMAT_STR = """
|
|
externalNativeBuild {{
|
|
cmake {{
|
|
{targets_section}
|
|
arguments {arguments}
|
|
}}
|
|
}}
|
|
"""
|
|
|
|
CUSTOM_GRADLE_COPY_NATIVE_CONFIG_FORMAT_STR = """
|
|
task copyNativeLibs{config}(type: Copy) {{
|
|
delete 'outputs/native-lib/{abi}'
|
|
|
|
from fileTree(dir: 'build/intermediates/cmake/{config_lower}/obj/arm64-v8a/{config_lower}', include: '**/*.so', exclude: 'lib{project_name}.GameLauncher.so' )
|
|
into 'outputs/native-lib/{abi}'
|
|
}}
|
|
|
|
compile{config}Sources.dependsOn copyNativeLibs{config}
|
|
|
|
copyNativeLibs{config}.mustRunAfter {{
|
|
tasks.findAll {{ task->task.name.contains('externalNativeBuild{config}') }}
|
|
}}
|
|
"""
|
|
|
|
CUSTOM_GRADLE_COPY_NATIVE_CONFIG_BUILD_ARTIFACTS_FORMAT_STR = """
|
|
task copyNativeArtifacts{config}(type: Copy) {{
|
|
from fileTree(dir: 'build/intermediates/cmake/{config_lower}/obj/arm64-v8a/{config_lower}', include: '{file_includes}' )
|
|
into '{asset_layout_folder}'
|
|
}}
|
|
|
|
compile{config}Sources.dependsOn copyNativeArtifacts{config}
|
|
|
|
copyNativeArtifacts{config}.mustRunAfter {{
|
|
tasks.findAll {{ task->task.name.contains('externalNativeBuild{config}') }}
|
|
}}
|
|
"""
|
|
|
|
CUSTOM_APPLY_ASSET_LAYOUT_TASK_FORMAT_STR = """
|
|
task syncLYLayoutMode{config}(type:Exec) {{
|
|
workingDir '{working_dir}'
|
|
commandLine '{python_full_path}', 'layout_tool.py', '--project-path', '{project_path}', '-p', 'Android', '-a', '{asset_type}', '-m', '{asset_mode}', '--create-layout-root', '-l', '{asset_layout_folder}'
|
|
}}
|
|
compile{config}Sources.dependsOn syncLYLayoutMode{config}
|
|
"""
|
|
|
|
|
|
PROJECT_DEPENDENCIES_VALUE_FORMAT = """
|
|
dependencies {{
|
|
{dependencies}
|
|
api 'androidx.core:core:1.1.0'
|
|
}}
|
|
"""
|
|
|
|
OVERRIDE_JAVA_SOURCESET_STR = """
|
|
java {{
|
|
srcDirs = ['{absolute_azandroid_path}', 'src/main/java']
|
|
}}
|
|
"""
|
|
|
|
class AndroidSigningConfig(object):
|
|
"""
|
|
Class to manage android signing configs
|
|
"""
|
|
def __init__(self, store_file, store_password, key_alias, key_password):
|
|
if not store_file:
|
|
raise common.LmbrCmdError(f"Keystore file not supplied for signing configuration",
|
|
common.ERROR_CODE_INVALID_PARAMETER)
|
|
if not os.path.isfile(store_file):
|
|
raise common.LmbrCmdError(f"Missing/Invalid keystore file {store_file} for signing config",
|
|
common.ERROR_CODE_INVALID_PARAMETER)
|
|
self.store_file = store_file.replace('\\', '/')
|
|
|
|
if not store_password:
|
|
raise common.LmbrCmdError(f"Keystore password not supplied for signing configuration",
|
|
common.ERROR_CODE_INVALID_PARAMETER)
|
|
self.store_password = store_password
|
|
|
|
if not key_alias:
|
|
raise common.LmbrCmdError(f"Signing key alias not supplied for signing configuration",
|
|
common.ERROR_CODE_INVALID_PARAMETER)
|
|
self.key_alias = key_alias
|
|
|
|
if not key_password:
|
|
raise common.LmbrCmdError(f"Signing key password not supplied for signing configuration",
|
|
common.ERROR_CODE_INVALID_PARAMETER)
|
|
self.key_password = key_password
|
|
|
|
def to_template_string(self, tabs):
|
|
tab_prefix = ' '*4*tabs
|
|
return f"""
|
|
{tab_prefix}storeFile file('{self.store_file}')
|
|
{tab_prefix}storePassword '{self.store_password}'
|
|
{tab_prefix}keyPassword '{self.key_password}'
|
|
{tab_prefix}keyAlias '{self.key_alias}'"""
|
|
|
|
|
|
class AndroidProjectGenerator(object):
|
|
"""
|
|
Class the manages the process to generate an android project folder in order to build with gradle/android studio
|
|
"""
|
|
|
|
def __init__(self, engine_root, build_dir, android_sdk_path, build_tool, android_sdk_platform, android_native_api_level, android_ndk,
|
|
project_path, third_party_path, cmake_version, override_cmake_path, override_gradle_path, gradle_version, gradle_plugin_version,
|
|
override_ninja_path, include_assets_in_apk, asset_mode, asset_type, signing_config, native_build_path, is_test_project=False,
|
|
overwrite_existing=True, unity_build_enabled=False):
|
|
"""
|
|
Initialize the object with all the required parameters needed to create an Android Project. The parameters should be verified before initializing this object
|
|
|
|
:param engine_root: The engine root that contains the engine
|
|
:param build_dir: The target folder under the where the android project folder will be created
|
|
:param android_sdk_path: The path to the ANDROID_SDK used for building the android java code
|
|
:param build_tool: The android SDK build-tool version.
|
|
:param android_sdk_platform: The android sdk platform version number to use for the Android SDK related builds
|
|
:param android_native_api_level:The android native API level (ANDROID_NATIVE_API_LEVEL) to set
|
|
:param android_ndk: The android ndk version number to use for the native builds
|
|
:param project_path: The path to the project
|
|
:param third_party_path: The required path to the lumberyard 3rd party path
|
|
:param cmake_version: The version number of cmake that will be used by gradle
|
|
:param override_cmake_path: The override path to cmake if it does not exists in the system path
|
|
:param override_gradle_path: The override path to gradle if it does not exists in the system path
|
|
:param gradle_version: The detected version of gradle being used
|
|
:param gradle_plugin_version: The android gradle plugin version
|
|
:param override_ninja_path: The override path to ninja if it does not exists in the system path
|
|
:param include_assets_in_apk:
|
|
:param asset_mode:
|
|
:param asset_type:
|
|
:param signing_config: Optional signing configuration arguments
|
|
:param is_test_project: Flag to indicate if this is a unit test runner project. (If true, project_path, asset_mode, asset_type, and include_assets_in_apk are ignored)
|
|
:param overwrite_existing: Flag to overwrite existing project files when being generated, or skip if they already exist.
|
|
"""
|
|
self.env = {}
|
|
|
|
self.engine_root = engine_root
|
|
|
|
self.build_dir = build_dir
|
|
|
|
self.android_sdk_path = android_sdk_path
|
|
|
|
self.android_project_builder_path = self.engine_root / 'Code/Tools/Android/ProjectBuilder'
|
|
|
|
self.android_sdk_platform = android_sdk_platform
|
|
self.android_sdk_build_tool_version = build_tool.version
|
|
|
|
self.android_ndk = android_ndk
|
|
self.android_ndk_version = android_ndk.version
|
|
self.android_native_api_level = android_native_api_level
|
|
|
|
self.project_path = project_path
|
|
|
|
self.third_party_path = third_party_path
|
|
|
|
self.cmake_version = cmake_version
|
|
|
|
self.override_cmake_path = override_cmake_path
|
|
|
|
self.override_gradle_path = override_gradle_path
|
|
|
|
self.gradle_version = gradle_version
|
|
|
|
self.gradle_plugin_version = gradle_plugin_version
|
|
|
|
self.override_ninja_path = override_ninja_path
|
|
|
|
self.include_assets_in_apk = include_assets_in_apk
|
|
|
|
self.native_build_path = native_build_path
|
|
|
|
self.asset_mode = asset_mode
|
|
|
|
self.asset_type = asset_type
|
|
|
|
self.signing_config = signing_config
|
|
|
|
self.is_test_project = is_test_project
|
|
|
|
self.overwrite_existing = overwrite_existing
|
|
|
|
self.unity_build_enabled = unity_build_enabled
|
|
|
|
def execute(self):
|
|
"""
|
|
Execute the android project creation workflow
|
|
"""
|
|
|
|
# Prepare the working build directory
|
|
self.build_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
self.create_platform_settings()
|
|
|
|
self.create_default_local_properties()
|
|
|
|
project_names = self.patch_and_transfer_android_libs()
|
|
|
|
project_names.extend(self.create_lumberyard_app(project_names))
|
|
|
|
root_gradle_env = {
|
|
'ANDROID_GRADLE_PLUGIN_VERSION': str(self.gradle_plugin_version),
|
|
'SDK_VER': self.android_sdk_platform,
|
|
'MIN_SDK_VER': self.android_sdk_platform,
|
|
'NDK_VERSION': self.android_ndk_version,
|
|
'SDK_BUILD_TOOL_VER': self.android_sdk_build_tool_version,
|
|
'LY_ENGINE_ROOT': common.normalize_path_for_settings(self.engine_root)
|
|
}
|
|
# Generate the gradle build script
|
|
self.create_file_from_project_template(src_template_file='root.build.gradle.in',
|
|
template_env=root_gradle_env,
|
|
dst_file=self.build_dir / 'build.gradle')
|
|
|
|
self.write_settings_gradle(project_names)
|
|
|
|
self.prepare_gradle_wrapper()
|
|
|
|
def create_file_from_project_template(self, src_template_file, template_env, dst_file):
|
|
"""
|
|
Create a file from an android template file
|
|
|
|
:param src_template_file: The name of the template file that is located under Code/Tools/Android/ProjectBuilder
|
|
:param template_env: The dictionary that contains the template substitution values
|
|
:param dst_file: The target concrete file to write to
|
|
"""
|
|
|
|
src_template_file_path = self.android_project_builder_path / src_template_file
|
|
if not dst_file.exists() or self.overwrite_existing:
|
|
|
|
default_local_properties_content = common.load_template_file(template_file_path=src_template_file_path,
|
|
template_env=template_env)
|
|
|
|
dst_file.write_text(default_local_properties_content,
|
|
encoding=common.DEFAULT_TEXT_WRITE_ENCODING,
|
|
errors=common.ENCODING_ERROR_HANDLINGS)
|
|
|
|
logging.info('Generated default {}'.format(dst_file.name))
|
|
else:
|
|
logging.info('Skipped {} (file exists)'.format(dst_file.name))
|
|
|
|
def prepare_gradle_wrapper(self):
|
|
"""
|
|
Generate the gradle wrapper by calling the validated version of gradle.
|
|
"""
|
|
logging.info('Preparing Gradle Wrapper')
|
|
|
|
if self.override_gradle_path:
|
|
gradle_wrapper_cmd = [self.override_gradle_path]
|
|
else:
|
|
gradle_wrapper_cmd = ['gradle']
|
|
|
|
gradle_wrapper_cmd.extend(['wrapper', '-p', str(self.build_dir.resolve())])
|
|
|
|
proc_result = subprocess.run(gradle_wrapper_cmd,
|
|
shell=True)
|
|
if proc_result.returncode != 0:
|
|
raise common.LmbrCmdError("Gradle was unable to generate a gradle wrapper for this project (code {}): {}"
|
|
.format(proc_result.returncode, proc_result.stderr or ""),
|
|
common.ERROR_CODE_ERROR_NOT_SUPPORTED)
|
|
|
|
def create_platform_settings(self):
|
|
"""
|
|
Create the 'platform.settings' file for the deployment script to use
|
|
"""
|
|
if self.is_test_project:
|
|
platform_settings_content = PLATFORM_SETTINGS_FORMAT.format(generation_timestamp=str(datetime.datetime.now().strftime("%c")),
|
|
platform='android',
|
|
project_path=self.project_path,
|
|
asset_mode='',
|
|
asset_type='',
|
|
android_sdk_path=str(self.android_sdk_path),
|
|
embed_assets_in_apk=True,
|
|
is_unit_test=True,
|
|
android_gradle_plugin_version=self.gradle_plugin_version)
|
|
else:
|
|
platform_settings_content = PLATFORM_SETTINGS_FORMAT.format(generation_timestamp=str(datetime.datetime.now().strftime("%c")),
|
|
platform='android',
|
|
project_path=self.project_path,
|
|
asset_mode=self.asset_mode,
|
|
asset_type=self.asset_type,
|
|
android_sdk_path=str(self.android_sdk_path),
|
|
embed_assets_in_apk=str(self.include_assets_in_apk),
|
|
is_unit_test=False,
|
|
android_gradle_plugin_version=self.gradle_plugin_version)
|
|
|
|
platform_settings_file = self.build_dir / 'platform.settings'
|
|
|
|
# Check if there already exists the build folder and a 'platform.settings' file. If there is an android gradle
|
|
# plugin version set and it is different than the one configured here, we will always overwrite it since
|
|
# there could be significant differences from one plug-in to the next
|
|
if platform_settings_file.is_file():
|
|
config = configparser.ConfigParser()
|
|
config.read([str(platform_settings_file.resolve(strict=True))])
|
|
if config.has_option('android', 'android_gradle_plugin'):
|
|
exist_agp_version = config.get('android', 'android_gradle_plugin')
|
|
if exist_agp_version != self.gradle_plugin_version:
|
|
self.overwrite_existing = True
|
|
|
|
platform_settings_file.open('w').write(platform_settings_content)
|
|
|
|
def create_default_local_properties(self):
|
|
"""
|
|
Create the default 'local.properties' file in the build folder
|
|
"""
|
|
template_android_sdk_path = common.normalize_path_for_settings(self.android_sdk_path, True)
|
|
if self.override_cmake_path:
|
|
# The cmake dir references the base cmake folder, not the executable path itself, so resolve to the base folder
|
|
template_cmake_path = common.normalize_path_for_settings(pathlib.Path(self.override_cmake_path).parent.parent, True)
|
|
else:
|
|
template_cmake_path = None
|
|
|
|
local_properties_env = {
|
|
"GENERATION_TIMESTAMP": str(datetime.datetime.now().strftime("%c")),
|
|
"ANDROID_SDK_PATH": template_android_sdk_path,
|
|
"CMAKE_DIR_LINE": f'cmake.dir={template_cmake_path}' if template_cmake_path else ''
|
|
}
|
|
|
|
self.create_file_from_project_template(src_template_file='local.properties.in',
|
|
template_env=local_properties_env,
|
|
dst_file=self.build_dir / 'local.properties')
|
|
|
|
def patch_and_transfer_android_libs(self):
|
|
"""
|
|
Patch and transfer android libraries from the android SDK path based on the rules outlined in Code/Tools/Android/ProjectBuilder/android_libraries.json
|
|
"""
|
|
|
|
# The android_libraries.json is templatized and needs to be provided the following environment for processing
|
|
# before we can process it.
|
|
android_libraries_substitution_table = {
|
|
"ANDROID_SDK_HOME": common.normalize_path_for_settings(self.android_sdk_path, False),
|
|
"ANDROID_SDK_VERSION": f"android-{self.android_sdk_platform}"
|
|
}
|
|
android_libraries_template_json_path = self.android_project_builder_path / ANDROID_LIBRARIES_JSON_FILE
|
|
|
|
android_libraries_template_json_content = android_libraries_template_json_path.resolve(strict=True) \
|
|
.read_text(encoding=common.DEFAULT_TEXT_READ_ENCODING,
|
|
errors=common.ENCODING_ERROR_HANDLINGS)
|
|
|
|
android_libraries_json_content = string.Template(android_libraries_template_json_content) \
|
|
.substitute(android_libraries_substitution_table)
|
|
|
|
android_libraries_json = json.loads(android_libraries_json_content)
|
|
|
|
# Process the android library rules
|
|
libs_to_patch = []
|
|
|
|
for libName, value in android_libraries_json.items():
|
|
# The library is in different places depending on the revision, so we must check multiple paths.
|
|
src_dir = None
|
|
for path in value['srcDir']:
|
|
path = string.Template(path).substitute(self.env)
|
|
if os.path.exists(path):
|
|
src_dir = path
|
|
break
|
|
|
|
if not src_dir:
|
|
raise common.LmbrCmdError('[ERROR] Failed to find library - {} - in path(s) [{}]. Please download the '
|
|
'library from the Android SDK Manager and run this command again.'
|
|
.format(libName, ", ".join(string.Template(path).substitute(self.env) for path in value['srcDir'])))
|
|
|
|
if 'patches' in value:
|
|
lib_to_patch = self._Library(libName, src_dir, self.overwrite_existing, self.signing_config)
|
|
for patch in value['patches']:
|
|
file_to_patch = self._File(patch['path'])
|
|
for change in patch['changes']:
|
|
line_num = change['line']
|
|
old_lines = change['old']
|
|
new_lines = change['new']
|
|
for oldLine in old_lines[:-1]:
|
|
change = self._Change(line_num, oldLine, (new_lines.pop() if new_lines else None))
|
|
file_to_patch.add_change(change)
|
|
line_num += 1
|
|
else:
|
|
change = self._Change(line_num, old_lines[-1], ('\n'.join(new_lines) if new_lines else None))
|
|
file_to_patch.add_change(change)
|
|
|
|
lib_to_patch.add_file_to_patch(file_to_patch)
|
|
lib_to_patch.dependencies = value.get('dependencies', [])
|
|
lib_to_patch.build_dependencies = value.get('buildDependencies', [])
|
|
|
|
libs_to_patch.append(lib_to_patch)
|
|
|
|
patched_lib_names = []
|
|
|
|
# Patch the libraries
|
|
for lib in libs_to_patch:
|
|
lib.process_patch_lib(android_project_builder_path=self.android_project_builder_path,
|
|
dest_root=self.build_dir)
|
|
patched_lib_names.append(lib.name)
|
|
|
|
return patched_lib_names
|
|
|
|
def create_lumberyard_app(self, project_dependencies):
|
|
"""
|
|
This will create the main lumberyard 'app' which will be packaged as an APK.
|
|
|
|
:param project_dependencies: Local project dependencies that may have been detected previously during construction of the android project folder
|
|
:returns List (of one) project name for the gradle build properties (see write_settings_gradle)
|
|
"""
|
|
|
|
az_android_dst_path = self.build_dir / APP_NAME
|
|
|
|
# We must always delete 'src' any existing copied AzAndroid projects since building may pick up stale java sources
|
|
lumberyard_app_src = az_android_dst_path / 'src'
|
|
if lumberyard_app_src.exists():
|
|
common.remove_dir_path(lumberyard_app_src)
|
|
|
|
logging.debug("Copying AzAndroid to '%s'", az_android_dst_path.resolve())
|
|
|
|
# The folder structure from the base lib needs to be mapped to a structure that gradle can process as a
|
|
# build project, and we need to generate some additional files
|
|
|
|
# Prepare the target folder
|
|
az_android_dst_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Prepare the 'PROJECT_DEPENDENCIES' environment variable
|
|
gradle_project_dependencies = [f" api project(path: ':{project_dependency}')" for project_dependency in project_dependencies]
|
|
|
|
template_engine_root = common.normalize_path_for_settings(self.engine_root)
|
|
template_third_party_path = common.normalize_path_for_settings(self.third_party_path)
|
|
template_ndk_path = common.normalize_path_for_settings(os.path.join(self.android_sdk_path, self.android_ndk.location))
|
|
template_unity_build = 1 if self.unity_build_enabled else 0
|
|
|
|
native_build_path = pathlib.Path(self.native_build_path).resolve().as_posix() if self.native_build_path else '.'
|
|
|
|
gradle_build_env = dict()
|
|
|
|
engine_root_as_path= pathlib.Path(self.engine_root)
|
|
|
|
absolute_cmakelist_path = (engine_root_as_path / 'CMakeLists.txt').resolve().as_posix()
|
|
absolute_azandroid_path = (engine_root_as_path / 'Code/Framework/AzAndroid/java').resolve().as_posix()
|
|
|
|
gradle_build_env['TARGET_TYPE'] = 'application'
|
|
gradle_build_env['PROJECT_DEPENDENCIES'] = PROJECT_DEPENDENCIES_VALUE_FORMAT.format(dependencies='\n'.join(gradle_project_dependencies))
|
|
gradle_build_env['NATIVE_CMAKE_SECTION_ANDROID'] = NATIVE_CMAKE_SECTION_ANDROID_FORMAT.format(cmake_version=str(self.cmake_version), native_build_path=native_build_path, absolute_cmakelist_path=absolute_cmakelist_path)
|
|
gradle_build_env['NATIVE_CMAKE_SECTION_DEFAULT_CONFIG'] = NATIVE_CMAKE_SECTION_DEFAULT_CONFIG_NDK_FORMAT_STR.format(abi=ANDROID_ARCH)
|
|
|
|
gradle_build_env['OVERRIDE_JAVA_SOURCESET'] = OVERRIDE_JAVA_SOURCESET_STR.format(absolute_azandroid_path=absolute_azandroid_path)
|
|
|
|
gradle_build_env['OPTIONAL_JNI_SRC_LIB_SET'] = ', "outputs/native-lib"'
|
|
|
|
for native_config in BUILD_CONFIGURATIONS:
|
|
|
|
native_config_upper = native_config.upper()
|
|
native_config_lower = native_config.lower()
|
|
|
|
# Prepare the cmake argument list based on the collected android settings and each build config
|
|
cmake_argument_list = [
|
|
'"-GNinja"',
|
|
f'"-S{template_engine_root}"',
|
|
f'"-DCMAKE_BUILD_TYPE={native_config_lower}"',
|
|
f'"-DCMAKE_TOOLCHAIN_FILE={template_engine_root}/cmake/Platform/Android/Toolchain_Android.cmake"',
|
|
f'"-DLY_3RDPARTY_PATH={template_third_party_path}"',
|
|
f'"-DLY_UNITY_BUILD={template_unity_build}"']
|
|
|
|
if not self.is_test_project:
|
|
cmake_argument_list.append(f'"-DLY_PROJECTS={pathlib.PurePath(self.project_path).as_posix()}"')
|
|
else:
|
|
cmake_argument_list.append('"-DLY_TEST_PROJECT=1"')
|
|
|
|
cmake_argument_list.extend([
|
|
f'"-DANDROID_NATIVE_API_LEVEL={self.android_native_api_level}"',
|
|
f'"-DLY_NDK_DIR={template_ndk_path}"',
|
|
'"-DANDROID_STL=c++_shared"',
|
|
'"-Wno-deprecated"',
|
|
])
|
|
if native_config == 'Release':
|
|
cmake_argument_list.append('"-DLY_MONOLITHIC_GAME=1"')
|
|
|
|
if self.override_ninja_path:
|
|
cmake_argument_list.append(f'"-DCMAKE_MAKE_PROGRAM={common.normalize_path_for_settings(self.override_ninja_path)}"')
|
|
|
|
# Query the project_path from the project.json file
|
|
project_name = common.read_project_name_from_project_json(self.project_path)
|
|
# Prepare the config-specific section to place the cmake argument list in the build.gradle for the app
|
|
gradle_build_env[f'NATIVE_CMAKE_SECTION_{native_config_upper}_CONFIG'] = \
|
|
NATIVE_CMAKE_SECTION_BUILD_TYPE_CONFIG_FORMAT_STR.format(targets_section=f'targets "{project_name}.GameLauncher"'
|
|
if project_name and not self.is_test_project else "", arguments=','.join(cmake_argument_list))
|
|
|
|
if project_name:
|
|
# Prepare the config-specific section to copy the related .so files that are marked as dependencies for the target
|
|
# (launcher) since gradle will not include them automatically for APK import
|
|
gradle_build_env[f'CUSTOM_GRADLE_COPY_NATIVE_{native_config_upper}_LIB_TASK'] = \
|
|
CUSTOM_GRADLE_COPY_NATIVE_CONFIG_FORMAT_STR.format(config=native_config,
|
|
config_lower=native_config_lower,
|
|
project_name=project_name,
|
|
abi=ANDROID_ARCH,
|
|
optional_test_excludes=",'*.Tests.so'" if not self.is_test_project else "")
|
|
|
|
if self.is_test_project:
|
|
gradle_build_env[f'CUSTOM_APPLY_ASSET_LAYOUT_{native_config_upper}_TASK'] = \
|
|
CUSTOM_GRADLE_COPY_NATIVE_CONFIG_BUILD_ARTIFACTS_FORMAT_STR.format(config=native_config,
|
|
config_lower=native_config_lower,
|
|
asset_layout_folder=(self.build_dir / 'app/src/main/assets').resolve().as_posix(),
|
|
file_includes='Test.Assets/**/*.*')
|
|
else:
|
|
# Copy over settings registry files from the Registry folder with build output directory
|
|
gradle_build_env[f'CUSTOM_APPLY_ASSET_LAYOUT_{native_config_upper}_TASK'] = \
|
|
CUSTOM_GRADLE_COPY_NATIVE_CONFIG_BUILD_ARTIFACTS_FORMAT_STR.format(config=native_config,
|
|
config_lower=native_config_lower,
|
|
asset_layout_folder=(self.build_dir / 'app/src/main/assets').resolve().as_posix(),
|
|
file_includes='**/Registry/*.setreg')
|
|
|
|
if self.include_assets_in_apk:
|
|
if not self.is_test_project:
|
|
gradle_build_env[f'CUSTOM_APPLY_ASSET_LAYOUT_{native_config_upper}_TASK'] += \
|
|
CUSTOM_APPLY_ASSET_LAYOUT_TASK_FORMAT_STR.format(working_dir=common.normalize_path_for_settings(self.engine_root / 'cmake/Tools'),
|
|
python_full_path=common.normalize_path_for_settings(self.engine_root / 'python' / PYTHON_SCRIPT),
|
|
asset_type=self.asset_type,
|
|
project_path=self.project_path.as_posix(),
|
|
asset_mode=self.asset_mode if native_config != 'Release' else 'PAK',
|
|
asset_layout_folder=(self.build_dir / 'app/src/main/assets').resolve().as_posix(),
|
|
config=native_config)
|
|
else:
|
|
gradle_build_env[f'CUSTOM_APPLY_ASSET_LAYOUT_{native_config_upper}_TASK'] = ''
|
|
if self.signing_config:
|
|
gradle_build_env[f'SIGNING_{native_config_upper}_CONFIG'] = f'signingConfig signingConfigs.{native_config_lower}' if self.signing_config else ''
|
|
else:
|
|
gradle_build_env[f'SIGNING_{native_config_upper}_CONFIG'] = ''
|
|
|
|
if self.signing_config:
|
|
gradle_build_env['SIGNING_CONFIGS'] = f"""
|
|
signingConfigs {{
|
|
debug {{{self.signing_config.to_template_string(3)}}}
|
|
profile {{{self.signing_config.to_template_string(3)}}}
|
|
release {{{self.signing_config.to_template_string(3)}}}
|
|
}}
|
|
"""
|
|
else:
|
|
gradle_build_env['SIGNING_CONFIGS'] = ""
|
|
|
|
az_android_gradle_file = az_android_dst_path / 'build.gradle'
|
|
self.create_file_from_project_template(src_template_file='build.gradle.in',
|
|
template_env=gradle_build_env,
|
|
dst_file=az_android_dst_path / 'build.gradle')
|
|
|
|
# Generate a AndroidManifest.xml and write to ${az_android_dst_path}/src/main/AndroidManifest.xml
|
|
dest_src_main_path = az_android_dst_path / 'src/main'
|
|
dest_src_main_path.mkdir(parents=True)
|
|
az_android_package_env = AndroidProjectManifestEnvironment(engine_root=self.engine_root,
|
|
project_path=self.project_path,
|
|
android_sdk_version_number=self.android_sdk_platform,
|
|
is_test=self.is_test_project)
|
|
self.create_file_from_project_template(src_template_file=ANDROID_MANIFEST_FILE,
|
|
template_env=az_android_package_env,
|
|
dst_file=dest_src_main_path / ANDROID_MANIFEST_FILE)
|
|
|
|
# Apply the 'android_builder.json' rules to copy over additional files to the target
|
|
self.apply_android_builder_rules(az_android_dst_path=az_android_dst_path,
|
|
az_android_package_env=az_android_package_env)
|
|
|
|
self.resolve_icon_overrides(az_android_dst_path=az_android_dst_path,
|
|
az_android_package_env=az_android_package_env)
|
|
|
|
self.resolve_splash_overrides(az_android_dst_path=az_android_dst_path,
|
|
az_android_package_env=az_android_package_env)
|
|
|
|
self.clear_unused_assets(az_android_dst_path=az_android_dst_path,
|
|
az_android_package_env=az_android_package_env)
|
|
|
|
return [APP_NAME]
|
|
|
|
def write_settings_gradle(self, project_list):
|
|
"""
|
|
Generate and write the 'settings.gradle' and 'gradle.properties file at the root of the android project folder
|
|
|
|
:param project_list: List of dependent projects to include in the gradle build
|
|
"""
|
|
|
|
settings_gradle_lines = [f"include ':{project_name}'" for project_name in project_list]
|
|
settings_gradle_content = '\n'.join(settings_gradle_lines)
|
|
settings_gradle_file = self.build_dir / 'settings.gradle'
|
|
settings_gradle_file.write_text(settings_gradle_content,
|
|
encoding=common.DEFAULT_TEXT_READ_ENCODING,
|
|
errors=common.ENCODING_ERROR_HANDLINGS)
|
|
logging.info("Generated settings.gradle -> %s", str(settings_gradle_file.resolve()))
|
|
|
|
# Write the default gradle.properties
|
|
|
|
# TODO: Add substitution entries here if variables are added to gradle.properties.in
|
|
# Refer to the Code/Tools/Android/ProjectBuilder/gradle.properties.in for reference.
|
|
grade_properties_env = {}
|
|
gradle_properties_file = self.build_dir / 'gradle.properties'
|
|
self.create_file_from_project_template(src_template_file='gradle.properties.in',
|
|
template_env=grade_properties_env,
|
|
dst_file=gradle_properties_file)
|
|
logging.info("Generated gradle.properties -> %s", str(gradle_properties_file.resolve()))
|
|
|
|
def apply_android_builder_rules(self, az_android_dst_path, az_android_package_env):
|
|
"""
|
|
Apply the 'android_builder.json' rule file that was used by WAF to prepare the gradle application apk file.
|
|
|
|
:param az_android_dst_path: The target application folder underneath the android target folder
|
|
:param az_android_package_env: The template environment to use to process all the source template files
|
|
"""
|
|
|
|
android_builder_json_path = self.android_project_builder_path / 'android_builder.json'
|
|
android_builder_json_content = common.load_template_file(template_file_path=android_builder_json_path,
|
|
template_env=az_android_package_env)
|
|
android_builder_json = json.loads(android_builder_json_content)
|
|
|
|
# Legacy files that don't need to be copied to the path (not needed for APK processing)
|
|
skip_files = ['wscript']
|
|
|
|
def _copy(src_file, dst_path, dst_is_directory):
|
|
"""
|
|
Perform a specialized copy
|
|
:param src_file: Source file to copy (relative to ${android_project_builder_path})
|
|
:param dst_path: The destination to copy to
|
|
:param dst_is_directory: Flag to indicate if the destination is a path or a file
|
|
"""
|
|
if src_file in skip_files:
|
|
# Filter out files that shouldn't be copied
|
|
return
|
|
|
|
src_path = self.android_project_builder_path / src_file
|
|
resolved_src = src_path.resolve(strict=True)
|
|
|
|
if imghdr.what(resolved_src) in ('rgb', 'gif', 'pbm', 'ppm', 'tiff', 'rast', 'xbm', 'jpeg', 'bmp', 'png'):
|
|
# If the source file is a binary asset, then perform a copy to the target path
|
|
logging.debug("Copy Binary file %s -> %s", str(src_path.resolve(strict=True)), str(dst_path.resolve()))
|
|
dst_path.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copyfile(resolved_src, dst_path.resolve())
|
|
else:
|
|
if dst_is_directory:
|
|
# If the dst_path is a directory, then we are copying the file to that directory
|
|
dst_path.mkdir(parents=True, exist_ok=True)
|
|
dst_file = dst_path / src_file
|
|
else:
|
|
# Otherwise, we are copying the file to the dst_path directly. A renaming may occur
|
|
dst_path.parent.mkdir(parents=True, exist_ok=True)
|
|
dst_file = dst_path
|
|
|
|
project_activity_for_game_content = common.load_template_file(template_file_path=src_path,
|
|
template_env=az_android_package_env)
|
|
dst_file.write_text(project_activity_for_game_content)
|
|
logging.debug("Copy/Update file %s -> %s", str(src_path.resolve(strict=True)), str(dst_path.resolve()))
|
|
|
|
def _process_dict(node, dst_path):
|
|
"""
|
|
Process a node from the android_builder.json file
|
|
:param node: The node to process
|
|
:param dst_path: The destination path derived from the node
|
|
"""
|
|
|
|
assert isinstance(node, dict), f"Node for {android_builder_json_path} expected to be a dictionary"
|
|
|
|
for key, value in node.items():
|
|
|
|
if isinstance(value, str):
|
|
_copy(key, dst_path / value, False)
|
|
|
|
elif isinstance(value, list):
|
|
for item in value:
|
|
assert isinstance(node, dict), f"Unexpected type found in '{android_builder_json_path}'. Only lists of strings are supported"
|
|
_copy(item, dst_path / key, True)
|
|
|
|
elif isinstance(value, dict):
|
|
_process_dict(value, dst_path / key)
|
|
else:
|
|
assert False, f"Unexpected type '{type(value)}' found in '{android_builder_json_path}'. Only str, list, and dict is supported"
|
|
|
|
_process_dict(android_builder_json, az_android_dst_path)
|
|
|
|
def construct_source_resource_path(self, source_path):
|
|
"""
|
|
Helper to construct the source path to an asset override such as
|
|
application icons or splash screen images
|
|
|
|
:param source_path: Source path or file to attempt to locate
|
|
:return: The path to the resource file
|
|
"""
|
|
if os.path.isabs(source_path):
|
|
# Always return itself if the path is already and absolute path
|
|
return pathlib.Path(source_path)
|
|
|
|
game_gem_resources = self.project_path / 'Gem' / 'Resources'
|
|
if game_gem_resources.is_dir(game_gem_resources):
|
|
# If the source is relative and the game gem's resource is present, construct the path based on that
|
|
return game_gem_resources / source_path
|
|
|
|
raise common.LmbrCmdError(f'Unable to locate resources folder for project at path "{self.project_path}"')
|
|
|
|
def resolve_icon_overrides(self, az_android_dst_path, az_android_package_env):
|
|
"""
|
|
Resolve any icon overrides
|
|
|
|
:param az_android_dst_path: The destination android path (app project folder)
|
|
:param az_android_package_env: Dictionary of env values to retrieve override information
|
|
"""
|
|
|
|
dst_resource_path = az_android_dst_path / 'src/main/res'
|
|
|
|
icon_overrides = az_android_package_env['ICONS']
|
|
if not icon_overrides:
|
|
return
|
|
|
|
# if a default icon is specified, then copy it into the generic mipmap folder
|
|
default_icon = icon_overrides.get('default', None)
|
|
|
|
if default_icon is not None:
|
|
|
|
src_default_icon_file = self.construct_source_resource_path(default_icon)
|
|
|
|
default_icon_target_dir = dst_resource_path / MIPMAP_PATH_PREFIX
|
|
default_icon_target_dir.mkdir(parents=True, exist_ok=True)
|
|
dst_default_icon_file = default_icon_target_dir / APP_ICON_NAME
|
|
|
|
shutil.copyfile(src_default_icon_file.resolve(), dst_default_icon_file.resolve())
|
|
os.chmod(dst_default_icon_file.resolve(), stat.S_IWRITE | stat.S_IREAD)
|
|
else:
|
|
logging.debug(f'No default icon override specified for project_at path {self.project_path}')
|
|
|
|
# process each of the resolution overrides
|
|
warnings = []
|
|
for resolution in ANDROID_RESOLUTION_SETTINGS:
|
|
|
|
target_directory = dst_resource_path / f'{MIPMAP_PATH_PREFIX}-{resolution}'
|
|
target_directory.mkdir(parent=True, exist_ok=True)
|
|
|
|
# get the current resolution icon override
|
|
icon_source = icon_overrides.get(resolution, default_icon)
|
|
if icon_source is default_icon:
|
|
|
|
# if both the resolution and the default are unspecified, warn the user but do nothing
|
|
if icon_source is None:
|
|
warnings.append(f'No icon override found for "{resolution}". Either supply one for "{resolution}" or a '
|
|
f'"default" in the android_settings "icon" section of the project.json file for {self.project_path}')
|
|
|
|
# if only the resolution is unspecified, remove the resolution specific version from the project
|
|
else:
|
|
logging.debug(f'Default icon being used for "{resolution}" in {self.project_path}', resolution)
|
|
common.remove_dir_path(target_directory)
|
|
continue
|
|
|
|
src_icon_file = self.construct_source_resource_path(icon_source)
|
|
dst_icon_file = target_directory / APP_ICON_NAME
|
|
|
|
shutil.copyfile(src_icon_file.resolve(), dst_icon_file.resolve())
|
|
os.chmod(dst_icon_file.resolve(), stat.S_IWRITE | stat.S_IREAD)
|
|
|
|
# guard against spamming warnings in the case the icon override block is full of garbage and no actual overrides
|
|
if len(warnings) != len(ANDROID_RESOLUTION_SETTINGS):
|
|
for warning_msg in warnings:
|
|
logging.warning(warning_msg)
|
|
|
|
def resolve_splash_overrides(self, az_android_dst_path, az_android_package_env):
|
|
"""
|
|
Resolve any splash screen overrides
|
|
|
|
:param az_android_dst_path: The destination android path (app project folder)
|
|
:param az_android_package_env: Dictionary of env values to retrieve override information
|
|
"""
|
|
|
|
dst_resource_path = az_android_dst_path / 'src/main/res'
|
|
|
|
splash_overrides = az_android_package_env['SPLASH_SCREEN']
|
|
if not splash_overrides:
|
|
return
|
|
|
|
orientation = az_android_package_env['ORIENTATION']
|
|
drawable_path_prefix = 'drawable-'
|
|
|
|
for orientation_flag, orientation_key in ORIENTATION_FLAG_TO_KEY_MAP.items():
|
|
orientation_path_prefix = drawable_path_prefix + orientation_key
|
|
|
|
oriented_splashes = splash_overrides.get(orientation_key, {})
|
|
|
|
unused_override_warning = None
|
|
if (orientation & orientation_flag) == 0:
|
|
unused_override_warning = f'Splash screen overrides specified for "{orientation_key}" when desired orientation ' \
|
|
f'is set to "{ORIENTATION_FLAG_TO_KEY_MAP[orientation]}" in project {self.project_path}. ' \
|
|
f'These overrides will be ignored.'
|
|
|
|
# if a default splash image is specified for this orientation, then copy it into the generic drawable-<orientation> folder
|
|
default_splash_img = oriented_splashes.get('default', None)
|
|
|
|
if default_splash_img is not None:
|
|
if unused_override_warning:
|
|
logging.warning(unused_override_warning)
|
|
continue
|
|
|
|
src_default_splash_img_file = self.construct_source_resource_path(default_splash_img)
|
|
|
|
dst_default_splash_img_dir = dst_resource_path / orientation_path_prefix
|
|
dst_default_splash_img_dir.mkdir(parents=True, exist_ok=True)
|
|
dst_default_splash_img_file = dst_default_splash_img_dir / APP_SPLASH_NAME
|
|
|
|
shutil.copyfile(src_default_splash_img_file.resolve(), dst_default_splash_img_file.resolve())
|
|
os.chmod(dst_default_splash_img_file.resolve(), stat.S_IWRITE | stat.S_IREAD)
|
|
else:
|
|
logging.debug(f'No default splash screen override specified for "%s" orientation in %s', orientation_key,
|
|
self.project_path)
|
|
|
|
# process each of the resolution overrides
|
|
warnings = []
|
|
|
|
# The xxxhdpi resolution is only for application icons, its overkill to include them for drawables... for now
|
|
valid_resolutions = set(ANDROID_RESOLUTION_SETTINGS)
|
|
valid_resolutions.discard('xxxhdpi')
|
|
|
|
for resolution in valid_resolutions:
|
|
target_directory = dst_resource_path / f'{orientation_path_prefix}-{resolution}'
|
|
|
|
# get the current resolution splash image override
|
|
splash_img_source = oriented_splashes.get(resolution, default_splash_img)
|
|
if splash_img_source is default_splash_img:
|
|
|
|
# if both the resolution and the default are unspecified, warn the user but do nothing
|
|
if splash_img_source is None:
|
|
section = f"{orientation_key}-{resolution}"
|
|
warnings.append(f'No splash screen override found for "{section}". Either supply one for "{resolution}" '
|
|
f'or a "default" in the android_settings "splash_screen-{orientation_key}" section of the '
|
|
f'project.json file for {self.project_path}.')
|
|
else:
|
|
# if only the resolution is unspecified, remove the resolution specific version from the project
|
|
logging.debug(f'Default splash screen being used for "{orientation_key}-{resolution}" in {self.project_path}')
|
|
common.remove_dir_path(target_directory)
|
|
continue
|
|
src_splash_img_file = self.construct_source_resource_path(splash_img_source)
|
|
dst_splash_img_file = target_directory / APP_SPLASH_NAME
|
|
|
|
shutil.copyfile(src_splash_img_file.resolve(), dst_splash_img_file.resolve())
|
|
os.chmod(dst_splash_img_file.resolve(), stat.S_IWRITE | stat.S_IREAD)
|
|
|
|
# guard against spamming warnings in the case the splash override block is full of garbage and no actual overrides
|
|
if len(warnings) != len(valid_resolutions):
|
|
if unused_override_warning:
|
|
logging.warning(unused_override_warning)
|
|
else:
|
|
for warning_msg in warnings:
|
|
logging.warning(warning_msg)
|
|
|
|
@staticmethod
|
|
def clear_unused_assets(az_android_dst_path, az_android_package_env):
|
|
"""
|
|
micro-optimization to clear assets from the final bundle that won't be used
|
|
|
|
:param az_android_dst_path: The destination android path (app project folder)
|
|
:param az_android_package_env: Dictionary of env values to retrieve override information
|
|
"""
|
|
|
|
orientation = az_android_package_env['ORIENTATION']
|
|
if orientation == ORIENTATION_LANDSCAPE:
|
|
path_prefix = 'drawable-land'
|
|
elif orientation == ORIENTATION_PORTRAIT:
|
|
path_prefix = 'drawable-port'
|
|
else:
|
|
return
|
|
|
|
# Prepare all the sub-folders to clear
|
|
clear_folders = [path_prefix]
|
|
clear_folders.extend([f'{path_prefix}-{resolution}' for resolution in ANDROID_RESOLUTION_SETTINGS if resolution != 'xxxhdpi'])
|
|
|
|
# Clear out the base folder
|
|
dst_resource_path = az_android_dst_path / 'src/main/res'
|
|
|
|
for clear_folder in clear_folders:
|
|
target_directory = dst_resource_path / clear_folder
|
|
if target_directory.is_dir():
|
|
logging.debug("Clearing folder %s", target_directory)
|
|
common.remove_dir_path(target_directory)
|
|
|
|
class _Library:
|
|
"""
|
|
Library class to manage the library node in android_libraries.json
|
|
"""
|
|
def __init__(self, name, path, overwrite_existing, signing_config=None):
|
|
self.name = name
|
|
self.path = path
|
|
self.signing_config = signing_config
|
|
self.overwrite_existing = overwrite_existing
|
|
self.patch_files = []
|
|
self.dependencies = []
|
|
self.build_dependencies = []
|
|
|
|
def add_file_to_patch(self, file):
|
|
self.patch_files.append(file)
|
|
|
|
def process_patch_lib(self, android_project_builder_path, dest_root):
|
|
"""
|
|
Perform the patch logic on the library node of 'android_libraries.json' (root level)
|
|
:param android_project_builder_path: Path to the Android/ProjectBuilder path for the templates
|
|
:param dest_root: The target android project folder
|
|
"""
|
|
|
|
# Clear out any existing target path's src and recreate
|
|
dst_path = dest_root / self.name
|
|
|
|
dst_path_src = dst_path / 'src'
|
|
if dst_path_src.exists():
|
|
common.remove_dir_path(dst_path_src)
|
|
dst_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
logging.debug("Copying library '{}' to '{}'".format(self.name, dst_path))
|
|
|
|
# The folder structure from the base lib needs to be mapped to a structure that gradle can process as a
|
|
# build project, and we need to generate some additional files
|
|
|
|
# Generate the gradle build script for the library based on the build.gradle.in template file
|
|
gradle_dependencies = []
|
|
if self.build_dependencies:
|
|
gradle_dependencies.extend([f" api '{build_dependency}'" for build_dependency in self.build_dependencies])
|
|
if self.dependencies:
|
|
gradle_dependencies.extend([f" api project(path: ':{dependency}')" for dependency in self.dependencies])
|
|
if gradle_dependencies:
|
|
project_dependencies = "dependencies {{\n{}\n}}".format('\n'.join(gradle_dependencies))
|
|
else:
|
|
project_dependencies = ""
|
|
|
|
# Prepare an environment for a basic, no-native (cmake) gradle project (java only)
|
|
build_gradle_env = {
|
|
'PROJECT_DEPENDENCIES': project_dependencies,
|
|
'TARGET_TYPE': 'library',
|
|
'NATIVE_CMAKE_SECTION_DEFAULT_CONFIG': '',
|
|
'NATIVE_CMAKE_SECTION_ANDROID': '',
|
|
'NATIVE_CMAKE_SECTION_DEBUG_CONFIG': '',
|
|
'NATIVE_CMAKE_SECTION_PROFILE_CONFIG': '',
|
|
'NATIVE_CMAKE_SECTION_RELEASE_CONFIG': '',
|
|
'OVERRIDE_JAVA_SOURCESET': '',
|
|
'OPTIONAL_JNI_SRC_LIB_SET': '',
|
|
|
|
'CUSTOM_APPLY_ASSET_LAYOUT_DEBUG_TASK': '',
|
|
'CUSTOM_APPLY_ASSET_LAYOUT_PROFILE_TASK': '',
|
|
'CUSTOM_APPLY_ASSET_LAYOUT_RELEASE_TASK': '',
|
|
|
|
'CUSTOM_GRADLE_COPY_NATIVE_DEBUG_LIB_TASK': '',
|
|
'CUSTOM_GRADLE_COPY_NATIVE_PROFILE_LIB_TASK': '',
|
|
'CUSTOM_GRADLE_COPY_NATIVE_RELEASE_LIB_TASK': '',
|
|
'SIGNING_CONFIGS': '',
|
|
'SIGNING_DEBUG_CONFIG': '',
|
|
'SIGNING_PROFILE_CONFIG': '',
|
|
'SIGNING_RELEASE_CONFIG': ''
|
|
}
|
|
build_gradle_content = common.load_template_file(template_file_path=android_project_builder_path / 'build.gradle.in',
|
|
template_env=build_gradle_env)
|
|
dest_gradle_script_file = dst_path / 'build.gradle'
|
|
if not dest_gradle_script_file.exists() or self.overwrite_existing:
|
|
dest_gradle_script_file.write_text(build_gradle_content,
|
|
encoding=common.DEFAULT_TEXT_WRITE_ENCODING,
|
|
errors=common.ENCODING_ERROR_HANDLINGS)
|
|
|
|
src_path = pathlib.Path(self.path)
|
|
|
|
# Prepare a 'src/main' folder
|
|
dst_src_main_path = dst_path / 'src/main'
|
|
dst_src_main_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Prepare a copy mapping list of tuples to process the copying of files and perform the straight file
|
|
# copying
|
|
library_copy_subfolder_pairs = [('res', 'res'),
|
|
('src', 'java')]
|
|
|
|
for copy_subfolder_pair in library_copy_subfolder_pairs:
|
|
|
|
src_subfolder = copy_subfolder_pair[0]
|
|
dst_subfolder = copy_subfolder_pair[1]
|
|
|
|
# {SRC}/{src_subfolder}/ -> {DST}/src/main/{dst_subfolder}/
|
|
src_library_res_path = src_path / src_subfolder
|
|
if not src_library_res_path.exists():
|
|
continue
|
|
dst_library_res_path = dst_src_main_path / dst_subfolder
|
|
shutil.copytree(src_library_res_path.resolve(),
|
|
dst_library_res_path.resolve(),
|
|
copy_function=shutil.copyfile)
|
|
|
|
# Process the files identified for patching
|
|
for file in self.patch_files:
|
|
|
|
input_file_path = src_path / file.path
|
|
if file.path == ANDROID_MANIFEST_FILE:
|
|
# Special case: AndroidManifest.xml does not go under the java/ parent path
|
|
output_file_path = dst_src_main_path / ANDROID_MANIFEST_FILE
|
|
else:
|
|
output_subpath = f"java{file.path[3:]}" # Strip out the 'src' from the library json and replace it with the target 'java' sub-path folder heading
|
|
output_file_path = dst_src_main_path / output_subpath
|
|
|
|
logging.debug(" Patching file '%s'", os.path.basename(file.path))
|
|
with open(input_file_path.resolve()) as input_file:
|
|
lines = input_file.readlines()
|
|
|
|
with open(output_file_path.resolve(), 'w') as outFile:
|
|
for replace in file.changes:
|
|
lines[replace.line] = str.replace(lines[replace.line], replace.old,
|
|
(replace.new if replace.new else ""), 1)
|
|
outFile.write(''.join([line for line in lines if line]))
|
|
|
|
class _File:
|
|
"""
|
|
Helper class to manage individual files for each library (_Library) and their changes
|
|
"""
|
|
def __init__(self, path):
|
|
self.path = path
|
|
self.changes = []
|
|
|
|
def add_change(self, change):
|
|
self.changes.append(change)
|
|
|
|
class _Change:
|
|
"""
|
|
Helper class to manage a change/patch as defined in android_libraries.json
|
|
"""
|
|
def __init__(self, line, old, new):
|
|
self.line = line
|
|
self.old = old
|
|
self.new = new
|
|
|
|
|
|
ANDROID_SDK_ENV_NAME = 'ANDROID_SDK'
|
|
|
|
|
|
def resolve_adb_tool(android_sdk_path):
|
|
"""
|
|
Resolve the location of the adb tool based on the input Android SDK Path
|
|
:param android_sdk_path: The android SDK path to search for the adb tool
|
|
:return: The absolute path to the adb tool
|
|
"""
|
|
|
|
if isinstance(android_sdk_path, str):
|
|
android_sdk_path = pathlib.Path(android_sdk_path)
|
|
|
|
file_found = False
|
|
for executable_path_ext in common.PLATFORM_EXECUTABLE_EXTENSIONS:
|
|
check_adb_target = android_sdk_path / 'platform-tools' / f'adb{executable_path_ext}'
|
|
if check_adb_target.is_file():
|
|
file_found = True
|
|
break
|
|
|
|
if not file_found:
|
|
raise common.LmbrCmdError(f"Invalid Android SDK path '{str(android_sdk_path)}': Unable to locate 'adb'.")
|
|
|
|
return check_adb_target
|
|
|
|
|
|
class AdbTool(common.CommandLineExec):
|
|
"""
|
|
Custom ADB command line processor
|
|
"""
|
|
|
|
def __init__(self, android_sdk_path):
|
|
|
|
check_adb_target = resolve_adb_tool(android_sdk_path)
|
|
|
|
super().__init__(str(check_adb_target))
|
|
|
|
self.is_connected = False
|
|
self.device_filter = None
|
|
|
|
def get_connected_device_serial_ids(self):
|
|
"""
|
|
Get the connected android device serial numbers through adb
|
|
:return: List of device serial numbers of android devices currently connected
|
|
"""
|
|
ret, devices_result, _ = super().exec(['devices'], capture_stdout=True)
|
|
if ret != 0:
|
|
raise common.LmbrCmdError("Unable to get device list from adb")
|
|
|
|
connected_device_serials = []
|
|
device_result_lines = [device_result_line.strip() for device_result_line in devices_result.split('\n') if
|
|
device_result_line]
|
|
for device_result_line in device_result_lines:
|
|
match_result = re.match(r'([\w-]+)\s+(device)', device_result_line)
|
|
if match_result:
|
|
device_id = match_result.group(1)
|
|
connected_device_serials.append(device_id)
|
|
return connected_device_serials
|
|
|
|
def connect(self, device_filter=None):
|
|
"""
|
|
Start the adb server
|
|
:param device_filter: Any device to filter subsequent commands to in situations where there may be multiple devices connected
|
|
"""
|
|
|
|
if self.is_connected:
|
|
raise common.LmbrCmdError("Adb connection already started")
|
|
|
|
super().exec(['start-server'])
|
|
|
|
if device_filter:
|
|
# If a device serial id was passed in, then verify if its valid
|
|
device_matched = False
|
|
connected_serial_ids = self.get_connected_device_serial_ids()
|
|
for connected_serial_id in connected_serial_ids:
|
|
if device_filter == connected_serial_id:
|
|
device_matched = True
|
|
break
|
|
|
|
if not device_matched:
|
|
raise common.LmbrCmdError(f"Invalid device serial {device_filter}. The current connected device serial ids are : {','.join(connected_serial_ids)}")
|
|
|
|
self.device_filter = device_filter
|
|
|
|
self.is_connected = True
|
|
|
|
def disconnect(self):
|
|
"""
|
|
Stop the adb server
|
|
"""
|
|
|
|
super().exec(['kill-server'])
|
|
self.is_connected = False
|
|
self.device_filter = None
|
|
|
|
def exec(self, arguments, capture_stdout=False, cwd=None):
|
|
"""
|
|
Wrapper to the base 'exec' call which may append an optional device filter to the adb calls
|
|
|
|
:param arguments: 'arguments' to pass to the base exec
|
|
:param capture_stdout: 'capture_stdout' to pass to the base exec
|
|
:param cwd: 'cwd' to pass to the base exec
|
|
:return: Result of the call (see common.CommandLineExec.exec)
|
|
"""
|
|
if self.device_filter:
|
|
adb_params = ['-s', self.device_filter]
|
|
adb_params.extend(arguments)
|
|
else:
|
|
adb_params = arguments
|
|
return super().exec(adb_params, capture_stdout, cwd)
|
|
|
|
def popen(self, arguments, cwd=None):
|
|
"""
|
|
Wrapper to the base 'popen' call which may append an optional device filter to the adb calls
|
|
|
|
:param arguments: 'arguments' to pass to the base popen
|
|
:param cwd: 'cwd' to pass to the base exec
|
|
:return: Result of the call (see common.CommandLineExec.popen)
|
|
"""
|
|
if self.device_filter:
|
|
adb_params = ['-s', self.device_filter]
|
|
adb_params.extend(arguments)
|
|
else:
|
|
adb_params = arguments
|
|
return super().popen(adb_params, cwd)
|
|
|
|
|
|
class AndroidGradlePluginInfo(object):
|
|
|
|
def __init__(self, android_gradle_plugin_version):
|
|
|
|
if android_gradle_plugin_version not in ANDROID_GRADLE_PLUGIN_COMPATIBILITY_MAP.keys():
|
|
raise common.LmbrCmdError(f"Android Gradle Plugin version {android_gradle_plugin_version} is not supported. "
|
|
f"Only the following version(s) are supported: {','.join(ANDROID_GRADLE_PLUGIN_COMPATIBILITY_MAP.keys())}")
|
|
|
|
details = ANDROID_GRADLE_PLUGIN_COMPATIBILITY_MAP[android_gradle_plugin_version]
|
|
self.default_sdk_build_tools_version = LooseVersion(details.get('sdk_build'))
|
|
|
|
self.default_ndk_version = LooseVersion(details.get('default_ndk'))
|
|
|
|
self.min_gradle_version = LooseVersion(details.get('min_gradle_version'))
|
|
|
|
self.min_cmake_version = LooseVersion(details.get('min_cmake_version'))
|
|
|
|
max_cmake_version_number = details.get('max_cmake_version')
|
|
self.max_cmake_version = None if max_cmake_version_number is None else LooseVersion(max_cmake_version_number)
|
|
|
|
|
|
class AndroidSDKResolver(object):
|
|
"""
|
|
Class that manages the Android SDK tool to validate, install packages (e.g. built tools, sdk platforms, ndk, etc)
|
|
"""
|
|
|
|
class InstalledPackage(object):
|
|
def __init__(self, installed_package_components):
|
|
assert len(installed_package_components) == 4, '4 sections expected for installed package components (path, version, description, location)'
|
|
self.path = installed_package_components[0]
|
|
self.version = LooseVersion(installed_package_components[1])
|
|
self.description = installed_package_components[2]
|
|
self.location = installed_package_components[3]
|
|
|
|
class AvailablePackage(object):
|
|
def __init__(self, available_package_components):
|
|
assert len(available_package_components) == 3, '3 sections expected for installed package components (path, version, description)'
|
|
self.path = available_package_components[0]
|
|
self.version = LooseVersion(available_package_components[1])
|
|
self.description = available_package_components[2]
|
|
|
|
class AvailableUpdate(object):
|
|
def __init__(self, available_update_components):
|
|
assert len(available_update_components) == 3, '3 sections expected for installed package components (path, version, available)'
|
|
self.path = available_update_components[0]
|
|
self.version = LooseVersion(available_update_components[1])
|
|
self.available = available_update_components[2]
|
|
|
|
def __init__(self, android_sdk_path):
|
|
|
|
self.android_sdk_path = android_sdk_path or os.environ.get(ANDROID_SDK_ENV_NAME)
|
|
if not self.android_sdk_path:
|
|
raise common.LmbrCmdError(f"Android SDK path not set or it was not passed into the command to generate the android project")
|
|
if not os.path.isdir(self.android_sdk_path):
|
|
raise common.LmbrCmdError(f"Android SDK path {self.android_sdk_path} is not valid")
|
|
if platform.system() == 'Windows':
|
|
self.sdk_manager_path = pathlib.Path(self.android_sdk_path) / 'tools' / 'bin' / 'sdkmanager.bat'
|
|
else:
|
|
raise common.LmbrCmdError(f"This tool is not supported on the current platform {platform.system()}")
|
|
if not self.sdk_manager_path.is_file():
|
|
raise common.LmbrCmdError(f"Android SDK path {self.android_sdk_path} is not valid or complete. Missing {self.sdk_manager_path}")
|
|
|
|
self.sdk_manager = common.CommandLineExec(str(self.sdk_manager_path.resolve()))
|
|
|
|
self.installed_packages = {}
|
|
self.available_packages = {}
|
|
self.available_updates = {}
|
|
self.refresh_sdk_installation()
|
|
|
|
def refresh_sdk_installation(self):
|
|
"""
|
|
Utilize the sdk_manager command line tool from the Android SDK to collect / refresh the list of
|
|
installed, available, and updateable packages that are managed by the android SDK.
|
|
"""
|
|
self.installed_packages = {}
|
|
self.available_packages = {}
|
|
self.available_updates = {}
|
|
|
|
def _factory_installed_package(package_map, item_components):
|
|
package_map[item_components[0]] = AndroidSDKResolver.InstalledPackage(item_components)
|
|
|
|
def _factory_available_package(package_map, item_components):
|
|
package_map[item_components[0]] = AndroidSDKResolver.AvailablePackage(item_components)
|
|
|
|
def _factory_available_update(package_map, item_components):
|
|
package_map[item_components[0]] = AndroidSDKResolver.AvailableUpdate(item_components)
|
|
|
|
# Use the SDK manager to collect the available and installed packages
|
|
result_code, result_stdout, result_stderr = self.sdk_manager.exec(['--list'], capture_stdout=True, suppress_stderr=True)
|
|
|
|
current_append_map = None
|
|
current_item_factory = None
|
|
for package_item in result_stdout.split('\n'):
|
|
package_item_stripped = package_item.strip()
|
|
if not package_item_stripped:
|
|
continue
|
|
if '|' not in package_item_stripped:
|
|
if package_item_stripped.upper() == 'INSTALLED PACKAGES:':
|
|
current_append_map = self.installed_packages
|
|
current_item_factory = _factory_installed_package
|
|
elif package_item_stripped.upper() == 'AVAILABLE PACKAGES:':
|
|
current_append_map = self.available_packages
|
|
current_item_factory = _factory_available_package
|
|
elif package_item_stripped.upper() == 'AVAILABLE UPDATES:':
|
|
current_append_map = self.available_updates
|
|
current_item_factory = _factory_available_update
|
|
else:
|
|
current_append_map = None
|
|
current_item_factory = None
|
|
continue
|
|
item_parts = [split.strip() for split in package_item_stripped.split('|')]
|
|
if len(item_parts) < 3:
|
|
continue
|
|
elif item_parts[1].upper() in ('VERSION', 'INSTALLED', '-------'):
|
|
continue
|
|
elif current_append_map is None:
|
|
continue
|
|
if current_append_map is not None and current_item_factory is not None:
|
|
current_item_factory(current_append_map, item_parts)
|
|
|
|
def is_package_installed(self, search_package_path):
|
|
"""
|
|
Check if a package path to see if its a package that is installed. The path can use wildcard '*'s
|
|
The function will return a list of the results that match the package paths, ordered by the newest version first
|
|
"""
|
|
def _package_sort(package):
|
|
return package.version
|
|
package_detail_result_list = []
|
|
for installed_package_path, installed_package_details in self.installed_packages.items():
|
|
if fnmatch.fnmatch(installed_package_path, search_package_path):
|
|
package_detail_result_list.append(installed_package_details)
|
|
package_detail_result_list.sort(reverse=True, key=_package_sort)
|
|
return package_detail_result_list
|
|
|
|
def is_package_available(self, search_package_path):
|
|
"""
|
|
Check if a package path to see if its an available package to install. The path can use wildcard '*'s
|
|
The function will return a list of the results that match the package paths, ordered by the newest version first
|
|
"""
|
|
def _package_sort(package):
|
|
return package.version
|
|
package_detail_result_list = []
|
|
for available_package_path, available_package_details in self.available_packages.items():
|
|
if fnmatch.fnmatch(available_package_path, search_package_path):
|
|
package_detail_result_list.append(available_package_details)
|
|
package_detail_result_list.sort(reverse=True, key=_package_sort)
|
|
return package_detail_result_list
|
|
|
|
def install_package(self, package_install_path, package_description):
|
|
"""
|
|
Install a package based on the path of an available android sdk package
|
|
"""
|
|
|
|
# Skip installation if the package is already installed
|
|
package_result_list = self.is_package_installed(package_install_path)
|
|
if package_result_list:
|
|
installed_package_detail = package_result_list[0]
|
|
logging.info(f"{installed_package_detail.description} (version {installed_package_detail.version}) Detected")
|
|
return installed_package_detail
|
|
|
|
# Make sure the package name is available
|
|
package_result_list = self.is_package_available(package_install_path)
|
|
if not package_result_list:
|
|
raise common.LmbrCmdError(f"Invalid Android SDK Package {package_description}: Bad package path {package_install_path}")
|
|
|
|
# Reverse sort and pick the first item, which should be the latest (if the install path contains wildcards)
|
|
def _available_sort(item):
|
|
return item.path
|
|
|
|
package_result_list.sort(reverse=True, key=_available_sort)
|
|
|
|
available_package_to_install = package_result_list[0] # For multiple hits, resolve to the first item which will be the latest version
|
|
|
|
# Perform the package installation
|
|
logging.info(f"Installing {available_package_to_install.description} ...")
|
|
result_code, result_stdout, result_stderr = self.sdk_manager.exec(['--install', available_package_to_install.path], capture_stdout=True, suppress_stderr=True)
|
|
if result_code != 0:
|
|
raise common.LmbrCmdError(f"Error installing package {available_package_to_install.path}: \n{result_stderr}")
|
|
|
|
# Refresh the tracked SDK Contents
|
|
self.refresh_sdk_installation()
|
|
|
|
# Get the package details to verify
|
|
package_result_list = self.is_package_installed(package_install_path)
|
|
if package_result_list:
|
|
installed_package_detail = package_result_list[0]
|
|
logging.info(f"{installed_package_detail.description} (version {installed_package_detail.version}) Installed")
|
|
return installed_package_detail
|
|
else:
|
|
raise common.LmbrCmdError(f"Error installing package {available_package_to_install.path}: \n{result_stderr}")
|
|
|