2 # Copyright (c) 2023 Valve Corporation
3 # Copyright (c) 2023 LunarG, Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 # NOTE: Android this documentation is crucial for understanding the layout of the NDK.
18 # https://android.googlesource.com/platform/ndk/+/master/docs/BuildSystemMaintainers.md
20 # NOTE: Environment variables we can rely on users/environments setting.
21 # https://github.com/actions/runner-images/blob/main/images/linux/Ubuntu2204-Readme.md#environment-variables-2
29 # helper to define paths relative to the repo root
30 def RepoRelative(path
):
31 return os
.path
.abspath(os
.path
.join(os
.path
.dirname(__file__
), '..', path
))
33 # Points to the directory containing the top level CMakeLists.txt
34 PROJECT_SRC_DIR
= os
.path
.abspath(os
.path
.join(os
.path
.split(os
.path
.abspath(__file__
))[0], '..'))
35 if not os
.path
.isfile(f
'{PROJECT_SRC_DIR}/CMakeLists.txt'):
36 print(f
'PROJECT_SRC_DIR invalid! {PROJECT_SRC_DIR}')
39 # Runs a command in a directory and returns its return code.
40 # Directory is project root by default, or a relative path from project root
41 def RunShellCmd(command
, start_dir
= PROJECT_SRC_DIR
, env
=None, verbose
=False):
42 # Flush stdout here. Helps when debugging on CI.
45 if start_dir
!= PROJECT_SRC_DIR
:
46 start_dir
= RepoRelative(start_dir
)
47 cmd_list
= command
.split(" ")
50 print(f
'CICMD({cmd_list}, env={env})')
51 subprocess
.check_call(cmd_list
, cwd
=start_dir
, env
=env
)
53 # Manifest file describing out test application
54 def get_android_manifest() -> str:
55 manifest
= RepoRelative('cube/android/AndroidManifest.xml')
56 if not os
.path
.isfile(manifest
):
57 print(f
"Unable to find manifest for APK! {manifest}")
61 # Generate the APK from the CMake binaries
62 def generate_apk(SDK_ROOT
: str, CMAKE_INSTALL_DIR
: str) -> str:
63 apk_dir
= RepoRelative('build-android/bin')
65 # Delete APK directory since it could contain files from old runs
66 if os
.path
.isdir(apk_dir
):
67 shutil
.rmtree(apk_dir
)
69 shutil
.copytree(CMAKE_INSTALL_DIR
, apk_dir
)
71 android_manifest
= get_android_manifest()
73 android_jar
= f
"{SDK_ROOT}/platforms/android-26/android.jar"
74 if not os
.path
.isfile(android_jar
):
75 print(f
"Unable to find {android_jar}!")
80 unaligned_apk
= f
'{apk_dir}/{apk_name}-unaligned.apk'
81 test_apk
= f
'{apk_dir}/{apk_name}.apk'
84 RunShellCmd(f
'aapt package -f -M {android_manifest} -I {android_jar} -F {unaligned_apk} {CMAKE_INSTALL_DIR}')
87 RunShellCmd(f
'zipalign -f 4 {unaligned_apk} {test_apk}')
89 # Create Key (If it doesn't already exist)
90 debug_key
= RepoRelative(f
'{apk_dir}/debug.keystore')
92 if not os
.path
.isfile(debug_key
):
93 dname
= 'CN=Android-Debug,O=Android,C=US'
94 RunShellCmd(f
'keytool -genkey -v -keystore {debug_key} -alias androiddebugkey -storepass {ks_pass} -keypass {ks_pass} -keyalg RSA -keysize 2048 -validity 10000 -dname {dname}')
97 RunShellCmd(f
'apksigner sign --verbose --ks {debug_key} --ks-pass pass:{ks_pass} {test_apk}')
99 # Android APKs can contain binaries for multiple ABIs (armeabi-v7a, arm64-v8a, x86, x86_64).
100 # https://en.wikipedia.org/wiki/Apk_(file_format)#Package_contents
102 # As a result CMake will need to be run multiple times to create a complete test APK that can be run on any Android device.
104 configs
= ['Release', 'Debug']
106 parser
= argparse
.ArgumentParser()
107 parser
.add_argument('--config', type=str, choices
=configs
, default
=configs
[0])
108 parser
.add_argument('--app-abi', dest
='android_abi', type=str, default
="arm64-v8a")
109 parser
.add_argument('--app-stl', dest
='android_stl', type=str, choices
=["c++_static", "c++_shared"], default
="c++_static")
110 parser
.add_argument('--apk', action
='store_true', help='Generate an APK as a post build step.')
111 parser
.add_argument('--clean', action
='store_true', help='Cleans CMake build artifacts')
112 args
= parser
.parse_args()
114 cmake_config
= args
.config
115 android_abis
= args
.android_abi
.split(" ")
116 android_stl
= args
.android_stl
117 create_apk
= args
.apk
120 if "ANDROID_NDK_HOME" not in os
.environ
:
121 print("Cannot find ANDROID_NDK_HOME!")
124 android_ndk_home
= os
.environ
.get('ANDROID_NDK_HOME')
125 android_toolchain
= f
'{android_ndk_home}/build/cmake/android.toolchain.cmake'
127 # The only tool we require for building is CMake/Ninja
128 required_cli_tools
= ['cmake', 'ninja']
130 # If we are building an APK we need a few more tools.
132 if "ANDROID_SDK_ROOT" not in os
.environ
:
133 print("Cannot find ANDROID_SDK_ROOT!")
136 android_sdk_root
= os
.environ
.get('ANDROID_SDK_ROOT')
137 print(f
"ANDROID_SDK_ROOT = {android_sdk_root}")
138 required_cli_tools
+= ['aapt', 'zipalign', 'keytool', 'apksigner']
140 print(f
"ANDROID_NDK_HOME = {android_ndk_home}")
141 print(f
"Build configured for {cmake_config} | {android_stl} | {android_abis} | APK {create_apk}")
143 if not os
.path
.isfile(android_toolchain
):
144 print(f
'Unable to find android.toolchain.cmake at {android_toolchain}')
147 for tool
in required_cli_tools
:
148 path
= shutil
.which(tool
)
150 print(f
"Unable to find {tool}!")
153 print(f
"Using {tool} : {path}")
155 cmake_install_dir
= RepoRelative('build-android/libs')
157 # Delete install directory since it could contain files from old runs
158 if os
.path
.isdir(cmake_install_dir
):
159 print("Cleaning CMake install")
160 shutil
.rmtree(cmake_install_dir
)
162 for abi
in android_abis
:
163 build_dir
= RepoRelative(f
'build-android/cmake/{abi}')
164 lib_dir
= f
'lib/{abi}'
167 print("Deleting CMakeCache.txt")
169 # Delete CMakeCache.txt to ensure clean builds
170 # NOTE: CMake 3.24 has --fresh which would be better to use in the future.
171 cmake_cache
= f
'{build_dir}/CMakeCache.txt'
172 if os
.path
.isfile(cmake_cache
):
173 os
.remove(cmake_cache
)
175 cmake_cmd
= f
'cmake -S . -B {build_dir} -G Ninja'
177 cmake_cmd
+= f
' -D CMAKE_BUILD_TYPE={cmake_config}'
178 cmake_cmd
+= f
' -D UPDATE_DEPS=ON -D UPDATE_DEPS_DIR={build_dir}'
179 cmake_cmd
+= f
' -D CMAKE_TOOLCHAIN_FILE={android_toolchain}'
180 cmake_cmd
+= f
' -D CMAKE_ANDROID_ARCH_ABI={abi}'
181 cmake_cmd
+= f
' -D CMAKE_INSTALL_LIBDIR={lib_dir}'
182 cmake_cmd
+= f
' -D CMAKE_ANDROID_STL_TYPE={android_stl}'
184 cmake_cmd
+= ' -D ANDROID_PLATFORM=26'
185 cmake_cmd
+= ' -D ANDROID_USE_LEGACY_TOOLCHAIN_FILE=NO'
187 RunShellCmd(cmake_cmd
)
189 build_cmd
= f
'cmake --build {build_dir}'
190 RunShellCmd(build_cmd
)
192 install_cmd
= f
'cmake --install {build_dir} --prefix {cmake_install_dir}'
193 RunShellCmd(install_cmd
)
196 generate_apk(SDK_ROOT
= android_sdk_root
, CMAKE_INSTALL_DIR
= cmake_install_dir
)
198 if __name__
== '__main__':