diff --git a/jitpack.yml b/jitpack.yml new file mode 100644 index 00000000000..66d6a310c51 --- /dev/null +++ b/jitpack.yml @@ -0,0 +1,17 @@ +# This file enables jitpack.io to build this repository into an AAR (Android +# Archive) for easier consumption by app developers. Instead of needing to +# download the VVL release artifacts from GitHub and check those libraries into +# their repository, they can instead depend on +# com.github.khronos:vulkan-validationlayers:$TAG. Jitpack will build the +# repository into an AAR and serve that artifact to developers. +# +# One caveat: if the VVL build is not completely deterministic (unlikely), the +# artifacts served from jitpack will not exactly match those hosted on the +# GitHub Release page, since jitpack will build the artifacts rather than serve +# the ones from the release. +# +# https://jitpack.io/docs/BUILDING/ +# https://github.com/KhronosGroup/Vulkan-ValidationLayers/issues/8167 +# https://github.com/KhronosGroup/Vulkan-ValidationLayers/pull/8303 +install: + - python scripts/jitpack.py diff --git a/scripts/android.py b/scripts/android.py index 7e59f4d95e0..d657ee7f65f 100755 --- a/scripts/android.py +++ b/scripts/android.py @@ -22,10 +22,15 @@ import argparse import os +from pathlib import Path import sys import shutil import common_ci + +MIN_SDK_VERSION = 26 + + # Manifest file describing out test application def get_android_manifest() -> str: manifest = common_ci.RepoRelative('tests/android/AndroidManifest.xml') @@ -77,23 +82,13 @@ def generate_apk(SDK_ROOT : str, CMAKE_INSTALL_DIR : str) -> str: # https://en.wikipedia.org/wiki/Apk_(file_format)#Package_contents # # 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. -def main(): - configs = ['Release', 'Debug', 'MinSizeRel'] - - parser = argparse.ArgumentParser() - parser.add_argument('--config', type=str, choices=configs, default=configs[0]) - parser.add_argument('--app-abi', dest='android_abi', type=str, default="arm64-v8a") - parser.add_argument('--app-stl', dest='android_stl', type=str, choices=["c++_static", "c++_shared"], default="c++_static") - parser.add_argument('--apk', action='store_true', help='Generate an APK as a post build step.') - parser.add_argument('--clean', action='store_true', help='Cleans CMake build artifacts') - args = parser.parse_args() - - cmake_config = args.config - android_abis = args.android_abi.split(" ") - android_stl = args.android_stl - create_apk = args.apk - clean = args.clean - +def build( + cmake_config: str, + android_abis: list[str], + android_stl: str, + create_apk: bool = False, + clean: bool = False, +) -> Path: if "ANDROID_NDK_HOME" not in os.environ: print("Cannot find ANDROID_NDK_HOME!") sys.exit(1) @@ -159,7 +154,7 @@ def main(): cmake_cmd += f' -D BUILD_TESTS={create_apk}' cmake_cmd += f' -D CMAKE_ANDROID_STL_TYPE={android_stl}' - cmake_cmd += ' -D ANDROID_PLATFORM=26' + cmake_cmd += f' -D ANDROID_PLATFORM={MIN_SDK_VERSION}' cmake_cmd += ' -D ANDROID_USE_LEGACY_TOOLCHAIN_FILE=NO' common_ci.RunShellCmd(cmake_cmd) @@ -175,5 +170,28 @@ def main(): if create_apk: generate_apk(SDK_ROOT = android_sdk_root, CMAKE_INSTALL_DIR = cmake_install_dir) + return Path(cmake_install_dir) + + +def main() -> None: + + configs = ['Release', 'Debug', 'MinSizeRel'] + + parser = argparse.ArgumentParser() + parser.add_argument('--config', type=str, choices=configs, default=configs[0]) + parser.add_argument('--app-abi', dest='android_abi', type=str, default="arm64-v8a") + parser.add_argument('--app-stl', dest='android_stl', type=str, choices=["c++_static", "c++_shared"], default="c++_static") + parser.add_argument('--apk', action='store_true', help='Generate an APK as a post build step.') + parser.add_argument('--clean', action='store_true', help='Cleans CMake build artifacts') + args = parser.parse_args() + + build( + args.config, + args.android_abi.split(" "), + args.android_sdk, + args.apk, + args.clean, + ) + if __name__ == '__main__': main() diff --git a/scripts/jitpack.py b/scripts/jitpack.py new file mode 100644 index 00000000000..43cfb408ff9 --- /dev/null +++ b/scripts/jitpack.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 LunarG, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Entry point for jitpack.yml. + +This implements the custom build command used by the jitpack.yml in the top level of +this repo. See the documentation in that file for more information. +""" +import json +import os +import subprocess +import sys +import textwrap +from collections.abc import Iterator +from pathlib import Path +from zipfile import ZipFile + +import android +import common_ci + + +def artifact_info_from_env() -> tuple[str, str, str]: + """Returns a tuple of [group ID, artifact ID, version].""" + # Jitpack puts this information in the environment: + # https://jitpack.io/docs/BUILDING/#build-environment + try: + return os.environ["GROUP"], os.environ["ARTIFACT"], os.environ["VERSION"] + except KeyError as ex: + sys.exit(f"{ex.args[0]} must be set in the environment") + + +def ndk_default_abis() -> Iterator[str]: + """Yields each default NDK ABI. + + The NDK includes a meta/abis.json file that enumerates each ABI the NDK is capable + of building for, and includes some descriptive data for each ABI. One of those + fields is "default", which will be true if the ABI is one that the NDK maintainers + recommend including support for by default. Most of the time all ABIs are + recommended to be built by default, but in the rare case where an ABI is under + development, or an old one is deprecated, we can key off that field to avoid them. + + At the time of writing (July 2024), the only non-default ABI is riscv64. The NDK r27 + changelog says that it's only there for bringup and shouldn't be used for release + artifacts yet. + """ + try: + ndk = Path(os.environ["ANDROID_NDK_HOME"]) + except KeyError: + sys.exit("ANDROID_NDK_HOME must be set in the environment") + + abis_meta_path = ndk / "meta/abis.json" + if not abis_meta_path.exists(): + sys.exit( + f"{abis_meta_path} does not exist. Does ANDROID_NDK_HOME ({ndk}) point to " + "a valid NDK?" + ) + + with abis_meta_path.open("r", encoding="utf-8") as abis_meta_file: + abis_meta = json.load(abis_meta_file) + + for name, data in abis_meta.items(): + if data["default"]: + yield name + + +def generate_aar( + output_path: Path, + library_dir: Path, + group_id: str, + artifact_id: str, + min_sdk_version: int, +) -> None: + """Creates an AAR from the CMake binaries.""" + with ZipFile(output_path, mode="w") as zip_file: + zip_file.writestr( + "AndroidManifest.xml", + textwrap.dedent( + f"""\ + + + + + """ + ), + ) + + for abi_dir in library_dir.iterdir(): + libs = list(abi_dir.glob("*.so")) + if not libs: + raise RuntimeError(f"No libraries found matching {abi_dir}/*.so") + for lib in libs: + zip_file.write(lib, arcname=f"jni/{abi_dir.name}/{lib.name}") + + +def install(aar: Path, group_id: str, artifact_id: str, version: str) -> None: + """Installs the AAR to the local maven repository.""" + # A custom jitpack build must install the artifact to the local maven repository. + # https://jitpack.io/docs/BUILDING/#custom-commands + # + # The jitpack host has mvn installed, so the easiest way to do this is to follow + # https://maven.apache.org/guides/mini/guide-3rd-party-jars-local.html + subprocess.run( + [ + "mvn", + "install:install-file", + f"-Dfile={aar}", + f"-DgroupId={group_id}", + f"-DartifactId={artifact_id}", + f"-Dversion={version}", + "-Dpackaging=aar", + ], + check=True, + ) + + +def main() -> None: + """Entry point called by jitpack.yml.""" + group_id, artifact_id, version = artifact_info_from_env() + build_dir = android.build("Release", list(ndk_default_abis()), "c++_static") + + aar_dir = Path(common_ci.RepoRelative("build-android/aar")) + aar_dir.mkdir(parents=True, exist_ok=True) + aar = aar_dir / f"{artifact_id}-{version}.aar" + generate_aar(aar, build_dir / "lib", group_id, artifact_id, android.MIN_SDK_VERSION) + install(aar, group_id, artifact_id, version) + + +if __name__ == "__main__": + main()