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