Last active
September 28, 2025 16:13
-
-
Save madebr/3563a41ec9d287b6754c5fe764f56a1c to your computer and use it in GitHub Desktop.
Create Android prefab (.aar) archives for native CMake projects
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python | |
| import argparse | |
| import glob | |
| from io import BytesIO | |
| import itertools | |
| import json | |
| import os | |
| from pathlib import Path | |
| import shlex | |
| import subprocess | |
| import textwrap | |
| import typing | |
| import zipfile | |
| ANDROID_ABIS = [ | |
| "armeabi-v7a", | |
| "arm64-v8a", | |
| "x86", | |
| "x86_64", | |
| "riscv64", | |
| ] | |
| def get_android_manifest(package_name: str, android_min_sdk: int, android_target_sdk: int) -> str: | |
| return textwrap.dedent(f"""\ | |
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | |
| package="{package_name}" | |
| android:versionCode="1" | |
| android:versionName="1.0"> | |
| <uses-sdk android:minSdkVersion="{android_min_sdk}" | |
| android:targetSdkVersion="{android_target_sdk}" /> | |
| </manifest> | |
| """) | |
| class Target: | |
| def __init__(self, name: str): | |
| self.name: str = name | |
| self.header_globs: list[Path] = [] | |
| self.headers_root: Path | None = None | |
| self.library: Path | None = None | |
| self.dependencies: list[str] = [] | |
| def is_static(self) -> bool: | |
| if self.library: | |
| return self.library.suffix == ".a" | |
| else: | |
| return False | |
| def __repr__(self) -> str: | |
| return f"<Target {self.name}>" | |
| class TargetAction(argparse.Action): | |
| def __call__(self, parser, namespace, value, option_string=None): | |
| for target in namespace.targets: | |
| if target.name == value: | |
| parser.error(f"Can only create one target names '{value}'") | |
| assert type(value) is str | |
| namespace.targets.append(Target(value)) | |
| class LibraryAction(argparse.Action): | |
| def __call__(self, parser, namespace, value, option_string=None): | |
| targets = getattr(namespace, "targets") | |
| if not targets: | |
| parser.error(f"{self.option_strings} needs a target") | |
| target = targets[-1] | |
| if target.library: | |
| parser.error(f"Can use {self.option_strings} only once per target") | |
| if value.suffix not in (".so", ".a"): | |
| parser.error(f"{self.option_strings} needs a path that ends with .so or .a") | |
| target.library = value | |
| class HeaderGlobsAction(argparse.Action): | |
| def __call__(self, parser, namespace, values, option_string=None): | |
| targets = getattr(namespace, "targets") | |
| if not targets: | |
| parser.error(f"{self.option_strings} needs a target") | |
| target = targets[-1] | |
| if target.header_globs: | |
| parser.error(f"Can use {self.option_strings} only once per target") | |
| target.header_globs = values | |
| class HeadersRootAction(argparse.Action): | |
| def __call__(self, parser, namespace, value, option_string=None): | |
| targets = getattr(namespace, "targets") | |
| if not targets: | |
| parser.error(f"{self.option_strings} needs a target") | |
| target = targets[-1] | |
| if target.headers_root: | |
| parser.error(f"Can use {self.option_strings} only once per target") | |
| target.headers_root = value | |
| class DependenciesAction(argparse.Action): | |
| def __call__(self, parser, namespace, values, option_string=None): | |
| targets = getattr(namespace, "targets") | |
| if not targets: | |
| parser.error(f"{self.option_strings} needs a target") | |
| target = targets[-1] | |
| if target.dependencies: | |
| parser.error(f"Can use {self.option_strings} only once per target") | |
| target.dependencies = values | |
| class FileTree: | |
| def __init__(self, namelist: list[str]): | |
| self.tree = {} | |
| for namepath in namelist: | |
| name_parts = namepath.split("/") | |
| current_name = None | |
| next_name = None | |
| for name_part in name_parts: | |
| if not next_name: | |
| next_name = name_part | |
| else: | |
| next_name = f"{next_name}/{name_part}" | |
| self.tree.setdefault(current_name, set()).add(name_part) | |
| current_name = next_name | |
| def walk(self, root=None) -> typing.Iterator[tuple[str, list[str], list[str]]]: | |
| if root not in self.tree: | |
| raise FileNotFoundError(root) | |
| def recursive_walk(dirname: str): | |
| path_items = self.tree[dirname] | |
| dirs = [] | |
| dir_paths = [] | |
| files = [] | |
| for item_name in path_items: | |
| item_path = f"{dirname}/{item_name}" | |
| if item_path in self.tree: | |
| dir_paths.append(item_path) | |
| dirs.append(item_name) | |
| else: | |
| files.append(item_name) | |
| yield dirname, dirs, files | |
| for dir_path in dir_paths: | |
| yield from recursive_walk(dir_path) | |
| yield from recursive_walk(dirname=root) | |
| def iterdir(self, path: str, allow_dirs=True, allow_files=True) -> typing.Iterator[str]: | |
| items = self.tree.get(path) | |
| if items is None: | |
| raise FileNotFoundError(path) | |
| for item in items: | |
| child_path = f"{path}/{item}" | |
| if not allow_dirs and child_path in self.tree: | |
| continue | |
| if not allow_files and child_path not in self.tree: | |
| continue | |
| yield item | |
| def is_file(self, path: str) -> bool: | |
| path_parts = path.rsplit("/", 1) | |
| if len(path_parts) == 1: | |
| return path in self.tree | |
| else: | |
| return path_parts[0] in self.tree and path_parts[1] in self.tree[path_parts[0]] | |
| def is_dir(self, path: str) -> bool: | |
| return path in self.tree | |
| class PrefabPackage: | |
| def __init__(self, name: str, version: str): | |
| self.name = name | |
| self.version = version | |
| self.modules: dict[str, PrefabModule] = {} | |
| self.dependencies: set[str] = set() | |
| class PrefabModule: | |
| def __init__(self, name: str, package: PrefabPackage): | |
| self.name = name | |
| self.package = package | |
| self.include_path: Path | None = None | |
| self.library: dict[str, Path] = {} | |
| self.dependencies: list[PrefabModule | str] = [] | |
| self.dependencies_todo: list[str] = [] | |
| def get_aar_name(aar_path: Path) -> str: | |
| with zipfile.ZipFile(aar_path, "r") as aar: | |
| prefab_json = json.load(aar.open("prefab/prefab.json")) | |
| return prefab_json["name"] | |
| def extract_aar_dep(aar_path: Path, extract_parent_path: Path) -> PrefabPackage: | |
| with zipfile.ZipFile(aar_path, "r") as aar: | |
| prefab_json = json.load(aar.open("prefab/prefab.json")) | |
| assert prefab_json["schema_version"] == 2 | |
| prefab_name = prefab_json["name"] | |
| prefab_version = prefab_json["version"] | |
| prefab_package = PrefabPackage(name=prefab_name, version=prefab_version) | |
| extract_path = extract_parent_path / prefab_name / "modules" | |
| file_tree = FileTree(aar.namelist()) | |
| for module_name in file_tree.iterdir("prefab/modules", allow_dirs=True, allow_files=False): | |
| prefab_module = PrefabModule(name=module_name, package=prefab_package) | |
| prefab_package.modules[module_name] = prefab_module | |
| aar_module_path = f"prefab/modules/{module_name}" | |
| module_json = json.load(aar.open(f"{aar_module_path}/module.json")) | |
| prefab_module.dependencies_todo.extend(module_json["export_libraries"]) | |
| module_libname = module_json.get("library_name") | |
| aar_module_include_path = f"{aar_module_path}/include" | |
| if file_tree.is_dir(aar_module_include_path): | |
| extract_include_path = extract_path / module_name / "include" | |
| prefab_module.include_path = extract_include_path | |
| for root, dirs, files in file_tree.walk(aar_module_include_path): | |
| include_dir = root.removeprefix(aar_module_include_path) | |
| include_dir = include_dir.removeprefix("/") | |
| extract_include = extract_include_path / include_dir | |
| extract_include.mkdir(parents=True, exist_ok=True) | |
| for file in files: | |
| (extract_include / file).write_bytes(aar.read(f"{root}/{file}")) | |
| aar_module_libs_path = f"{aar_module_path}/libs" | |
| if file_tree.is_dir(aar_module_libs_path): | |
| extract_lib_path = extract_path / module_name / "lib" | |
| for module_lib_dirname in file_tree.iterdir(aar_module_libs_path, allow_dirs=True, allow_files=False): | |
| aar_module_abi_lib_path = f"{aar_module_libs_path}/{module_lib_dirname}" | |
| abi_json = json.load(aar.open(f"{aar_module_abi_lib_path}/abi.json")) | |
| abi = abi_json["abi"] | |
| assert abi not in prefab_module.library | |
| module_static = abi_json["static"] | |
| lib_suffix = ".a" if module_static else ".so" | |
| extract_abi_lib_path = extract_lib_path / abi / f"{module_libname}{lib_suffix}" | |
| prefab_module.library[abi] = extract_abi_lib_path | |
| aar_lib_path = f"{aar_module_abi_lib_path}/{module_libname}{lib_suffix}" | |
| (extract_lib_path / abi).mkdir(parents=True, exist_ok=True) | |
| extract_abi_lib_path.write_bytes(aar.read(aar_lib_path)) | |
| return prefab_package | |
| def write_cmake_config_version(path: Path, version: str): | |
| with path.open("w") as f: | |
| f.write(textwrap.dedent(f"""\ | |
| set(PACKAGE_VERSION "{version}") | |
| if (PACKAGE_FIND_VERSION_RANGE) | |
| # Package version must be in the requested version range | |
| if ((PACKAGE_FIND_VERSION_RANGE_MIN STREQUAL "INCLUDE" AND PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION_MIN) | |
| OR ((PACKAGE_FIND_VERSION_RANGE_MAX STREQUAL "INCLUDE" AND PACKAGE_VERSION VERSION_GREATER PACKAGE_FIND_VERSION_MAX) | |
| OR (PACKAGE_FIND_VERSION_RANGE_MAX STREQUAL "EXCLUDE" AND PACKAGE_VERSION VERSION_GREATER_EQUAL PACKAGE_FIND_VERSION_MAX))) | |
| set(PACKAGE_VERSION_COMPATIBLE FALSE) | |
| else() | |
| set(PACKAGE_VERSION_COMPATIBLE TRUE) | |
| endif() | |
| else() | |
| if(PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION) | |
| set(PACKAGE_VERSION_COMPATIBLE FALSE) | |
| else() | |
| set(PACKAGE_VERSION_COMPATIBLE TRUE) | |
| if(PACKAGE_FIND_VERSION STREQUAL PACKAGE_VERSION) | |
| set(PACKAGE_VERSION_EXACT TRUE) | |
| endif() | |
| endif() | |
| endif() | |
| """)) | |
| def main(): | |
| parser = argparse.ArgumentParser(allow_abbrev=False) | |
| parser.add_argument("--name", required=True, help="Name of package (e.g. 'Sublimo')") | |
| parser.add_argument("--version", required=True, help="Verson of package (e.g. '1.2.3')") | |
| parser.add_argument("--dependencies", nargs="*", default=[], help="Package dependencies. You must provide a aar for each. (e.g. SDL3)") | |
| parser.add_argument("--android-package-name", required=True, help="Java-style package name (e.g. 'com.example.sublimo.android')") | |
| parser.add_argument("--android-min-sdk", type=int, default=18) | |
| parser.add_argument("--android-target-sdk", type=int, default=18) | |
| parser.add_argument("--cmake-source-dir", type=Path, required=True, help="Source dir of CMake project") | |
| parser.add_argument("--cmake-common-args", type=str, required=True, help="Common CMake configuration arguments (templated)") | |
| parser.add_argument("--license-paths", type=Path, default=[], nargs="+", help="License files for inclusion (templated)") | |
| parser.add_argument("--dep-aars", type=Path, default=[], nargs="+") | |
| parser.add_argument("--java-sources-globs", type=Path, default=[], nargs="+", help="Globs for java compilation") | |
| parser.add_argument("--java-version", default="1.8", help="Target Java version") | |
| parser.add_argument("--java-javac", default="javac", help="Path of javac compiler") | |
| parser.add_argument("--java-jar", default="jar", help="Path of jar archiver") | |
| parser.add_argument("--java-classpaths", type=Path, default=[], nargs="+", help="Extra Java classpaths. Must contain path of android.jar, often found at $ANDROID_HOME/Sdk/platforms/android-N/android.jar") | |
| parser.add_argument("--add-target", action=TargetAction, help="Create a new target (can be used multiple times)") | |
| parser.add_argument("--target-set-library", type=Path, action=LibraryAction, help="Set library of current target (can be used once per target) (templated)") | |
| parser.add_argument("--target-set-headers", nargs="+", type=Path, action=HeaderGlobsAction, help="Set headers (can be used once per target) (templated)") | |
| parser.add_argument("--target-set-headers-root", type=Path, action=HeadersRootAction, help="Set headers root directory (can be used once per target) (templated)") | |
| parser.add_argument("--target-set-dependencies", nargs="+", type=str, action=DependenciesAction, help="Set dependencies of current target (can be used once per target)") | |
| parser.add_argument("--build-dir", type=Path, required=True, help="Path where to store all intermediate and final artifacts") | |
| parser.add_argument("--verbose", action="store_true", help="Verbose output") | |
| parser.set_defaults(targets=[]) | |
| args = parser.parse_args() | |
| if not args.cmake_source_dir.is_dir(): | |
| parser.error(f"--cmake-source-dir must be an existing directory: {args.cmake_source_dir}") | |
| if not args.build_dir.is_dir(): | |
| parser.error(f"--build-dir must be an existing directory: {args.cmake_source_dir}") | |
| if args.verbose: | |
| subprocess_output_stream = None | |
| else: | |
| subprocess_output_stream = subprocess.DEVNULL | |
| for target in args.targets: | |
| print(f"- {target}") | |
| print(f" header globs: {target.header_globs}") | |
| print(f" headers_root: {target.headers_root}") | |
| print(f" library: {target.library}") | |
| print(f" dependencies: {target.dependencies}") | |
| available_dep_aar_paths: dict[str, Path] = {} | |
| for dep_aar_path in args.dep_aars: | |
| dep_aar_name = get_aar_name(dep_aar_path) | |
| if dep_aar_name in available_dep_aar_paths: | |
| parser.error(f"Package {dep_aar_name} is provided by \"{dep_aar_path}\" and \"{available_dep_aar_paths['dep_aar_name']}\"") | |
| available_dep_aar_paths[dep_aar_name] = dep_aar_path | |
| dep_packages_done = set() | |
| dep_packages_todo = set() | |
| java_sources = [] | |
| classes_jar_path: Path | None = None | |
| for java_sources_glob in args.java_sources_globs: | |
| java_sources.extend(glob.glob(str(java_sources_glob))) | |
| if java_sources: | |
| java_build_path = args.build_dir / f"{args.name}-java-build" | |
| java_classpaths = os.pathsep.join(str(p) for p in args.java_classpaths) | |
| javac_cmd = [ | |
| args.java_javac, | |
| "--source", args.java_version, "--target", args.java_version, | |
| "-d", str(java_build_path), | |
| ] | |
| if java_classpaths: | |
| javac_cmd.extend(["--class-path", java_classpaths]) | |
| javac_cmd += java_sources | |
| print("Compiling Java sources ...") | |
| if args.verbose: | |
| print(javac_cmd) | |
| subprocess.check_call(javac_cmd, stdout=subprocess_output_stream, stderr=subprocess_output_stream) | |
| java_classes = [] | |
| for root, _, files in java_build_path.walk(): | |
| java_classes.extend([root.relative_to(java_build_path) / f for f in files if f.endswith(".class")]) | |
| classes_jar_path = java_build_path / "classes.jar" | |
| jar_cmd = [args.java_jar, "--create", "--file", str(classes_jar_path)] + java_classes | |
| print("Creating classes.jar ...") | |
| if args.verbose: | |
| print(jar_cmd) | |
| subprocess.check_call(jar_cmd, stdout=subprocess_output_stream, stderr=subprocess_output_stream, cwd=java_build_path) | |
| for dependency in args.dependencies: | |
| if dependency not in available_dep_aar_paths: | |
| parser.error(f"No aar satisfies dependency \"{dependency}\"") | |
| dep_packages_todo.update(args.dependencies) | |
| deps_root_path = args.build_dir / f"{args.name}-android-deps" | |
| dep_aar_packages: dict[str, PrefabPackage] = {} | |
| while dep_packages_todo: | |
| current_dep_package_name = dep_packages_todo.pop() | |
| if current_dep_package_name not in available_dep_aar_paths: | |
| parser.error(f"No aar satisfies \"{current_dep_package_name}\" dependency") | |
| dep_aar_path = available_dep_aar_paths[current_dep_package_name] | |
| print(f"Extracting {current_dep_package_name} from \"{dep_aar_path}\"") | |
| dep_aar_package = extract_aar_dep(aar_path=dep_aar_path, extract_parent_path=deps_root_path) | |
| dep_aar_packages[dep_aar_package.name] = dep_aar_package | |
| dep_packages_done.add(current_dep_package_name) | |
| dep_packages_todo.update(dep_aar_package.dependencies) | |
| dep_packages_todo.difference_update(dep_packages_done) | |
| for aar_package in dep_aar_packages.values(): | |
| for aar_module in aar_package.modules.values(): | |
| for module_dep_todo in aar_module.dependencies_todo: | |
| if module_dep_todo.startswith(":"): | |
| module_dep_name = module_dep_todo.removeprefix(":") | |
| aar_module.dependencies.append(aar_package.modules[module_dep_name]) | |
| elif module_dep_todo.startswith("//"): | |
| package_module_dep_name = module_dep_todo.removeprefix("//") | |
| package_and_module_name = package_module_dep_name.split("/", 1) | |
| if len(package_and_module_name) != 2: | |
| parser.error(f"Package {aar_package.name}, module {aar_module.name} has an invalid dependency \"{module_dep_todo}\"") | |
| dep_package_name, dep_module_name = package_and_module_name | |
| aar_module.dependencies.append(dep_aar_packages[dep_package_name].modules[dep_module_name]) | |
| else: | |
| aar_module.dependencies.append(module_dep_todo) | |
| aar_module.dependencies_todo.clear() | |
| cmake_config_paths: dict[str, dict[str, Path]] = {} | |
| for aar_package_name, aar_package in dep_aar_packages.items(): | |
| dep_cmake_dir = deps_root_path / aar_package_name / "cmake" | |
| # dep_cmake_dir.mkdir(parents=True, exist_ok=True) | |
| abi_modules: dict[str, list[PrefabModule]] = {} | |
| abi_all_modules: list[PrefabModule] = [] | |
| for dep_module in aar_package.modules.values(): | |
| for abi, path in dep_module.library.items(): | |
| abi_modules.setdefault(abi, []).append(dep_module) | |
| if not dep_module.library: | |
| abi_all_modules.append(dep_module) | |
| for abi, dep_modules in abi_modules.items(): | |
| config_path = dep_cmake_dir / abi | |
| cmake_config_paths.setdefault(abi, {})[aar_package_name] = config_path | |
| config_path.mkdir(parents=True, exist_ok=True) | |
| write_cmake_config_version(path=config_path / f"{aar_package_name}ConfigVersion.cmake", version=aar_package.version) | |
| with (config_path / f"{aar_package_name}Config.cmake").open("w") as config: | |
| for dep_module in itertools.chain(dep_modules, abi_all_modules): | |
| if dep_module.library.get(abi): | |
| library_type = "STATIC" if dep_module.library[abi].suffix == ".a" else "SHARED" | |
| else: | |
| library_type = "INTERFACE" | |
| target = f"{aar_package_name}::{dep_module.name}" | |
| config.write(f"# Target {target}\n") | |
| config.write(f"if(NOT TARGET {target})\n") | |
| config.write(f" add_library({target} {library_type} IMPORTED)\n") | |
| if dep_module.library.get(abi): | |
| safe_location = str(dep_module.library[abi]).replace("\\", "/") | |
| config.write(f" set_property(TARGET {target} PROPERTY IMPORTED_LOCATION \"{safe_location}\")\n") | |
| if dep_module.include_path: | |
| safe_include_path = str(dep_module.include_path).replace("\\", "/") | |
| config.write(f" set_property(TARGET {target} PROPERTY INTERFACE_INCLUDE_DIRECTORIES \"{safe_include_path}\")\n") | |
| link_libraries: list[str] = [] | |
| link_options: list[str] = [] | |
| for module_dependency in dep_module.dependencies: | |
| if isinstance(module_dependency, PrefabModule): | |
| link_libraries.append(f"{module_dependency.package.name}::{module_dependency.name}") | |
| elif module_dependency.startswith("-l"): | |
| link_libraries.append(module_dependency.removeprefix("-l")) | |
| else: | |
| link_options.append(module_dependency) | |
| if link_libraries: | |
| config.write(f" set_property(TARGET {target} PROPERTY INTERFACE_LINK_LIBRARIES \"{';'.join(link_libraries)}\")\n") | |
| if link_options: | |
| config.write(f" set_property(TARGET {target} PROPERTY INTERFACE_LINK_OPTIONS \"{';'.join(link_options)}\")\n") | |
| config.write("endif()\n") | |
| config.write("\n") | |
| aar_raw_data = BytesIO() | |
| with (zipfile.ZipFile(aar_raw_data, mode="w", compression=zipfile.ZIP_DEFLATED) as aar_file): | |
| aar_file.writestr("AndroidManifest.xml", get_android_manifest( | |
| package_name=args.android_package_name, | |
| android_min_sdk=args.android_min_sdk, | |
| android_target_sdk=args.android_target_sdk, | |
| )) | |
| prefab_json = { | |
| "schema_version": 2, | |
| "name": args.name, | |
| "version": args.version, | |
| "dependencies": args.dependencies, | |
| } | |
| aar_file.writestr("prefab/prefab.json", json.dumps(prefab_json, indent=4)) | |
| single_action_done = False | |
| for android_abi in ANDROID_ABIS: | |
| source_dir = args.cmake_source_dir | |
| build_dir = args.build_dir / f"{args.name}-ndk-build-{android_abi}" | |
| install_dir = args.build_dir / f"{args.name}-ndk-install-{android_abi}" | |
| def repl(s: str) -> str: | |
| s = s.replace("{SOURCE_DIR}", str(source_dir)) | |
| s = s.replace("{PREFIX_DIR}", str(install_dir)) | |
| s = s.replace("{BUILD_DIR}", str(build_dir)) | |
| s = s.replace("{NAME}", args.name) | |
| s = s.replace("{VERSION}", args.version) | |
| s = s.replace("{MINUS}", "-") | |
| return s | |
| cmake_configure_args = ["cmake"] | |
| cmake_configure_args.extend(shlex.split(args.cmake_common_args)) | |
| cmake_configure_args.extend([ | |
| "-S", str(source_dir), | |
| "-B", str(build_dir), | |
| f"-DCMAKE_INSTALL_PREFIX={install_dir}", | |
| f"-DANDROID_ABI={android_abi}", | |
| ]) | |
| for name, config_path in cmake_config_paths.get(android_abi, {}).items(): | |
| cmake_configure_args.append(f"-D{name}_DIR={config_path}") | |
| cmake_configure_args = [repl(a) for a in cmake_configure_args] | |
| cmake_build_args = ["cmake", "--build", str(build_dir)] | |
| cmake_install_args = ["cmake", "--install", str(build_dir)] | |
| print(f"Configuring {android_abi}...") | |
| if args.verbose: | |
| print("args:", cmake_configure_args) | |
| subprocess.check_call(cmake_configure_args, stdout=subprocess_output_stream, stderr=subprocess_output_stream) | |
| print(f"Building {android_abi}...") | |
| subprocess.check_call(cmake_build_args, stdout=subprocess_output_stream, stderr=subprocess_output_stream) | |
| print(f"Installing {android_abi}...") | |
| subprocess.check_call(cmake_install_args, stdout=subprocess_output_stream, stderr=subprocess_output_stream) | |
| for target in args.targets: | |
| prefab_module_dir = f"prefab/modules/{target.name}" | |
| if not single_action_done: | |
| module_json: dict[str, object] = { | |
| "export_libraries": list(repl(s) for s in target.dependencies), | |
| } | |
| if target.library: | |
| library = Path(repl(str(target.library))) | |
| module_json.update({ | |
| "library_name": library.stem, | |
| }) | |
| aar_file.writestr(f"{prefab_module_dir}/module.json", json.dumps(module_json, indent=4)) | |
| if target.header_globs: | |
| header_root = None | |
| if target.headers_root: | |
| header_root = Path(repl(str(target.headers_root))) | |
| for header_glob in target.header_globs: | |
| header_glob = repl(str(header_glob)) | |
| for header in glob.glob(header_glob): | |
| header = Path(header) | |
| if header_root: | |
| dest_name = header.relative_to(header_root) | |
| else: | |
| dest_name = header.name | |
| aar_file.write(header, f"{prefab_module_dir}/include/{dest_name}") | |
| else: | |
| aar_file.writestr(f"{prefab_module_dir}/include/.keep", "") | |
| if target.library: | |
| aar_lib_dir = f"{prefab_module_dir}/libs/android.{android_abi}" | |
| library = Path(repl(str(target.library))) | |
| aar_file.write(library, f"{aar_lib_dir}/{library.name}") | |
| abi_json = { | |
| "abi": android_abi, | |
| "api": args.android_min_sdk, # ? | |
| "ndk": args.android_min_sdk, # ? | |
| "stl": "none", # FIXME! | |
| "static": target.is_static(), | |
| } | |
| aar_file.writestr(f"{aar_lib_dir}/abi.json", json.dumps(abi_json, indent=4)) | |
| if not single_action_done: | |
| if classes_jar_path: | |
| aar_file.write(classes_jar_path, "classes.jar") | |
| for license_path in args.license_paths: | |
| license_path = Path(repl(str(license_path))) | |
| if not license_path.is_file(): | |
| parser.error(f"{license_path} does not exist") | |
| aar_file.write(license_path, f"META-INF/{license_path.name}") | |
| single_action_done = True | |
| if not single_action_done: | |
| parser.error("Nothing done") | |
| aar_path = Path(args.build_dir / f"{args.name}-{args.version}.aar") | |
| with aar_path.open("wb") as f: | |
| aar_raw_data.seek(0) | |
| f.write(aar_raw_data.read()) | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I used this command for Windows to build Box2D v3 AAR:
create-prefab-aar.py --verbose --cmake-source-dir H:/libs/box2d-build/tmp/box2d --cmake-common-args "-GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DANDROID_USE_LEGACY_TOOLCHAIN_FILE=FALSE -DCMAKE_TOOLCHAIN_FILE=H:/Android/SDK/ndk/23.1.7779620/build/cmake/android.toolchain.cmake -GNinja -DBOX2D_SAMPLES=OFF -DBOX2D_VALIDATE=OFF -DBOX2D_UNIT_TESTS=OFF -DBUILD_SHARED_LIBS=ON -DCMAKE_C_FLAGS=\"-ffile-prefix-map={SOURCE_DIR}=/src/{NAME}-{VERSION}\"" --name box2d --version 3.2.0 --add-target box2d --target-set-headers {PREFIX_DIR}/include/box2d/* --target-set-headers-root {PREFIX_DIR}/include --target-set-library {PREFIX_DIR}/lib/libbox2d.so --build-dir H:/libs/box2d-build/tmp/android-build --android-package-name org.box2d.android --license-paths {SOURCE_DIR}/LICENSEThe command above is based on the Linux command that was published on the official SDL Discord server here:
create-prefab-aar.py --cmake-source-dir /tmp/box2d --cmake-common-args "-GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DANDROID_USE_LEGACY_TOOLCHAIN_FILE=FALSE -DCMAKE_TOOLCHAIN_FILE=/home/maarten/Android/Sdk/ndk/27.0.11718014/build/cmake/android.toolchain.cmake -GNinja -DBOX2D_SAMPLES=OFF -DBOX2D_VALIDATE=OFF -DBOX2D_UNIT_TESTS=OFF -DBUILD_SHARED_LIBS=ON -DCMAKE_C_FLAGS=\"-ffile-prefix-map=<SOURCE_DIR>=/src/<NAME>-<VERSION>\"" --name box2d --version 3.2.0 --add-target box2d --target-set-headers <PREFIX_DIR>/include/box2d/* --target-set-headers-root <PREFIX_DIR>/include --target-set-library <PREFIX_DIR>/lib/libbox2d.so --build-dir /tmp/android-build --android-package-name org.box2d.android --license-paths <SOURCE_DIR>/LICENSEBox2D v3 AAR file was attached on the official SDL Discord server here.
I prefer to use absolute paths in the example https://github.com/madebr/sdlcross to include libs in
android-project/app/build.gradle:Because libs require 55 MB:
When absolute paths are used, space and time are saved because I can keep all libs in the one general libs folder in one place.
I used these commits:
The example play-sound-by-touch-android-box2dv3-sdl3-c based on the SDLCross