create-android-project.py 7.9 KB

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