Skip to content

Instantly share code, notes, and snippets.

@madebr
Last active September 28, 2025 16:13
Show Gist options
  • Save madebr/3563a41ec9d287b6754c5fe764f56a1c to your computer and use it in GitHub Desktop.
Save madebr/3563a41ec9d287b6754c5fe764f56a1c to your computer and use it in GitHub Desktop.
Create Android prefab (.aar) archives for native CMake projects
#!/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())
@8Observer8
Copy link

8Observer8 commented Sep 27, 2025

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}/LICENSE

The 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>/LICENSE

Box2D 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:

dependencies {
    implementation files('H:/libs/SDL3-devel-3.2.22-android/SDL3-3.2.22.aar')
    implementation files('H:/libs/SDL3_image-devel-3.2.4-android/SDL3_image-3.2.4.aar')
    implementation files('H:/libs/SDL3_mixer-devel-3.1.0-android/SDL3_mixer-3.1.0.aar')
    implementation files('H:/libs/SDL3_ttf-devel-3.2.2-android/SDL3_ttf-3.2.2.aar')
    /* Use [create-prefab-aar.py](https://gist.github.com/madebr/3563a41ec9d287b6754c5fe764f56a1c)
     * to creat a Android prefab archive for Box2D.
     */
    implementation files('H:/libs/box2d-3.2.0-android/box2d-3.2.0.aar')
    implementation 'androidx.appcompat:appcompat:1.5.1'
}

Because libs require 55 MB:

image

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment