Simple Directmedia Layer
at main 9.0 kB view raw
1#!/usr/bin/env python3 2import os 3from argparse import ArgumentParser 4from pathlib import Path 5import re 6import shutil 7import sys 8import textwrap 9 10 11SDL_ROOT = Path(__file__).resolve().parents[1] 12 13def extract_sdl_version() -> str: 14 """ 15 Extract SDL version from SDL3/SDL_version.h 16 """ 17 18 with open(SDL_ROOT / "include/SDL3/SDL_version.h") as f: 19 data = f.read() 20 21 major = int(next(re.finditer(r"#define\s+SDL_MAJOR_VERSION\s+([0-9]+)", data)).group(1)) 22 minor = int(next(re.finditer(r"#define\s+SDL_MINOR_VERSION\s+([0-9]+)", data)).group(1)) 23 micro = int(next(re.finditer(r"#define\s+SDL_MICRO_VERSION\s+([0-9]+)", data)).group(1)) 24 return f"{major}.{minor}.{micro}" 25 26def replace_in_file(path: Path, regex_what: str, replace_with: str) -> None: 27 with path.open("r") as f: 28 data = f.read() 29 30 new_data, count = re.subn(regex_what, replace_with, data) 31 32 assert count > 0, f"\"{regex_what}\" did not match anything in \"{path}\"" 33 34 with open(path, "w") as f: 35 f.write(new_data) 36 37 38def android_mk_use_prefab(path: Path) -> None: 39 """ 40 Replace relative SDL inclusion with dependency on prefab package 41 """ 42 43 with path.open() as f: 44 data = "".join(line for line in f.readlines() if "# SDL" not in line) 45 46 data, _ = re.subn("[\n]{3,}", "\n\n", data) 47 48 data, count = re.subn(r"(LOCAL_SHARED_LIBRARIES\s*:=\s*SDL3)", "LOCAL_SHARED_LIBRARIES := SDL3 SDL3-Headers", data) 49 assert count == 1, f"Must have injected SDL3-Headers in {path} exactly once" 50 51 newdata = data + textwrap.dedent(""" 52 # https://google.github.io/prefab/build-systems.html 53 54 # Add the prefab modules to the import path. 55 $(call import-add-path,/out) 56 57 # Import SDL3 so we can depend on it. 58 $(call import-module,prefab/SDL3) 59 """) 60 61 with path.open("w") as f: 62 f.write(newdata) 63 64 65def cmake_mk_no_sdl(path: Path) -> None: 66 """ 67 Don't add the source directories of SDL/SDL_image/SDL_mixer/... 68 """ 69 70 with path.open() as f: 71 lines = f.readlines() 72 73 newlines: list[str] = [] 74 for line in lines: 75 if "add_subdirectory(SDL" in line: 76 while newlines[-1].startswith("#"): 77 newlines = newlines[:-1] 78 continue 79 newlines.append(line) 80 81 newdata, _ = re.subn("[\n]{3,}", "\n\n", "".join(newlines)) 82 83 with path.open("w") as f: 84 f.write(newdata) 85 86 87def gradle_add_prefab_and_aar(path: Path, aar: str) -> None: 88 with path.open() as f: 89 data = f.read() 90 91 data, count = re.subn("android {", textwrap.dedent(""" 92 android { 93 buildFeatures { 94 prefab true 95 }"""), data) 96 assert count == 1 97 98 data, count = re.subn("dependencies {", textwrap.dedent(f""" 99 dependencies {{ 100 implementation files('libs/{aar}')"""), data) 101 assert count == 1 102 103 with path.open("w") as f: 104 f.write(data) 105 106 107def gradle_add_package_name(path: Path, package_name: str) -> None: 108 with path.open() as f: 109 data = f.read() 110 111 data, count = re.subn("org.libsdl.app", package_name, data) 112 assert count >= 1 113 114 with path.open("w") as f: 115 f.write(data) 116 117 118def main() -> int: 119 description = "Create a simple Android gradle project from input sources." 120 epilog = textwrap.dedent("""\ 121 You need to manually copy a prebuilt SDL3 Android archive into the project tree when using the aar variant. 122 123 Any changes you have done to the sources in the Android project will be lost 124 """) 125 parser = ArgumentParser(description=description, epilog=epilog, allow_abbrev=False) 126 parser.add_argument("package_name", metavar="PACKAGENAME", help="Android package name (e.g. com.yourcompany.yourapp)") 127 parser.add_argument("sources", metavar="SOURCE", nargs="*", help="Source code of your application. The files are copied to the output directory.") 128 parser.add_argument("--variant", choices=["copy", "symlink", "aar"], default="copy", help="Choose variant of SDL project (copy: copy SDL sources, symlink: symlink SDL sources, aar: use Android aar archive)") 129 parser.add_argument("--output", "-o", default=SDL_ROOT / "build", type=Path, help="Location where to store the Android project") 130 parser.add_argument("--version", default=None, help="SDL3 version to use as aar dependency (only used for aar variant)") 131 132 args = parser.parse_args() 133 if not args.sources: 134 print("Reading source file paths from stdin (press CTRL+D to stop)") 135 args.sources = [path for path in sys.stdin.read().strip().split() if path] 136 if not args.sources: 137 parser.error("No sources passed") 138 139 if not os.getenv("ANDROID_HOME"): 140 print("WARNING: ANDROID_HOME environment variable not set", file=sys.stderr) 141 if not os.getenv("ANDROID_NDK_HOME"): 142 print("WARNING: ANDROID_NDK_HOME environment variable not set", file=sys.stderr) 143 144 args.sources = [Path(src) for src in args.sources] 145 146 build_path = args.output / args.package_name 147 148 # Remove the destination folder 149 shutil.rmtree(build_path, ignore_errors=True) 150 151 # Copy the Android project 152 shutil.copytree(SDL_ROOT / "android-project", build_path) 153 154 # Add the source files to the ndk-build and cmake projects 155 replace_in_file(build_path / "app/jni/src/Android.mk", r"YourSourceHere\.c", " \\\n ".join(src.name for src in args.sources)) 156 replace_in_file(build_path / "app/jni/src/CMakeLists.txt", r"YourSourceHere\.c", "\n ".join(src.name for src in args.sources)) 157 158 # Remove placeholder source "YourSourceHere.c" 159 (build_path / "app/jni/src/YourSourceHere.c").unlink() 160 161 # Copy sources to output folder 162 for src in args.sources: 163 if not src.is_file(): 164 parser.error(f"\"{src}\" is not a file") 165 shutil.copyfile(src, build_path / "app/jni/src" / src.name) 166 167 sdl_project_files = ( 168 SDL_ROOT / "src", 169 SDL_ROOT / "include", 170 SDL_ROOT / "LICENSE.txt", 171 SDL_ROOT / "README.md", 172 SDL_ROOT / "Android.mk", 173 SDL_ROOT / "CMakeLists.txt", 174 SDL_ROOT / "cmake", 175 ) 176 if args.variant == "copy": 177 (build_path / "app/jni/SDL").mkdir(exist_ok=True, parents=True) 178 for sdl_project_file in sdl_project_files: 179 # Copy SDL project files and directories 180 if sdl_project_file.is_dir(): 181 shutil.copytree(sdl_project_file, build_path / "app/jni/SDL" / sdl_project_file.name) 182 elif sdl_project_file.is_file(): 183 shutil.copyfile(sdl_project_file, build_path / "app/jni/SDL" / sdl_project_file.name) 184 elif args.variant == "symlink": 185 (build_path / "app/jni/SDL").mkdir(exist_ok=True, parents=True) 186 # Create symbolic links for all SDL project files 187 for sdl_project_file in sdl_project_files: 188 os.symlink(sdl_project_file, build_path / "app/jni/SDL" / sdl_project_file.name) 189 elif args.variant == "aar": 190 if not args.version: 191 args.version = extract_sdl_version() 192 193 major = args.version.split(".")[0] 194 aar = f"SDL{ major }-{ args.version }.aar" 195 196 # Remove all SDL java classes 197 shutil.rmtree(build_path / "app/src/main/java") 198 199 # Use prefab to generate include-able files 200 gradle_add_prefab_and_aar(build_path / "app/build.gradle", aar=aar) 201 202 # Make sure to use the prefab-generated files and not SDL sources 203 android_mk_use_prefab(build_path / "app/jni/src/Android.mk") 204 cmake_mk_no_sdl(build_path / "app/jni/CMakeLists.txt") 205 206 aar_libs_folder = build_path / "app/libs" 207 aar_libs_folder.mkdir(parents=True) 208 with (aar_libs_folder / "copy-sdl-aars-here.txt").open("w") as f: 209 f.write(f"Copy {aar} to this folder.\n") 210 211 print(f"WARNING: copy { aar } to { aar_libs_folder }", file=sys.stderr) 212 213 # Add the package name to build.gradle 214 gradle_add_package_name(build_path / "app/build.gradle", args.package_name) 215 216 # Create entry activity, subclassing SDLActivity 217 activity = args.package_name[args.package_name.rfind(".") + 1:].capitalize() + "Activity" 218 activity_path = build_path / "app/src/main/java" / args.package_name.replace(".", "/") / f"{activity}.java" 219 activity_path.parent.mkdir(parents=True) 220 with activity_path.open("w") as f: 221 f.write(textwrap.dedent(f""" 222 package {args.package_name}; 223 224 import org.libsdl.app.SDLActivity; 225 226 public class {activity} extends SDLActivity 227 {{ 228 }} 229 """)) 230 231 # Add the just-generated activity to the Android manifest 232 replace_in_file(build_path / "app/src/main/AndroidManifest.xml", 'name="SDLActivity"', f'name="{activity}"') 233 234 # Update project and build 235 print("To build and install to a device for testing, run the following:") 236 print(f"cd {build_path}") 237 print("./gradlew installDebug") 238 return 0 239 240if __name__ == "__main__": 241 raise SystemExit(main())