From 50ae47af5e67099f9e99f96440c4159d3e87c88d Mon Sep 17 00:00:00 2001 From: Anonymous Maarten Date: Fri, 5 Jul 2024 00:09:40 +0200 Subject: [PATCH] android: create android project in create-android-project.py python script This script supersedes androidbuild.sh, and also supports using a SDL3 prefab archive --- .github/workflows/android.yml | 6 +- .github/workflows/release.yml | 69 +++++++- android-project/app/jni/CMakeLists.txt | 5 - android-project/app/jni/src/Android.mk | 13 +- build-scripts/androidbuild.sh | 106 ------------ build-scripts/build-release.py | 2 +- build-scripts/create-android-project.py | 217 ++++++++++++++++++++++++ docs/README-android.md | 10 +- 8 files changed, 303 insertions(+), 125 deletions(-) delete mode 100755 build-scripts/androidbuild.sh create mode 100755 build-scripts/create-android-project.py diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index f6940806f..70029d686 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -40,7 +40,11 @@ jobs: - name: Create Gradle project if: ${{ matrix.platform.gradle }} run: | - build-scripts/androidbuild.sh org.libsdl.testspriteminimal test/testspriteminimal.c test/icon.h + python build-scripts/create-android-project.py \ + --output "build" \ + --variant copy \ + org.libsdl.testspriteminimal \ + test/testspriteminimal.c test/icon.h echo "" echo "Project contents:" echo "" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4f79f6465..e9fdc3b13 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -461,7 +461,6 @@ jobs: sparse-checkout: 'build-scripts/build-release.py' - name: 'Setup Android NDK' uses: nttld/setup-ndk@v1 - id: setup_ndk with: local-cache: true ndk-version: r21e @@ -500,3 +499,71 @@ jobs: with: name: android path: '${{ github.workspace }}/dist' + + android-verify: + needs: [android, src] + runs-on: ubuntu-latest + steps: + - name: 'Set up Python' + uses: actions/setup-python@v5 + with: + python-version: '3.10' + - name: 'Setup Android NDK' + uses: nttld/setup-ndk@v1 + with: + local-cache: true + ndk-version: r21e + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + - name: 'Download source archives' + uses: actions/download-artifact@v4 + with: + name: sources + path: '${{ github.workspace }}' + - name: 'Download Android .aar archive' + uses: actions/download-artifact@v4 + with: + name: android + path: '${{ github.workspace }}' + - name: 'Untar ${{ needs.src.outputs.src-tar-gz }}' + id: src + run: | + mkdir -p /tmp/tardir + tar -C /tmp/tardir -v -x -f "${{ github.workspace }}/${{ needs.src.outputs.src-tar-gz }}" + echo "path=/tmp/tardir/${{ needs.src.outputs.project }}-${{ needs.src.outputs.version }}" >>$GITHUB_OUTPUT + - name: 'Create gradle project' + id: create-gradle-project + run: | + python ${{ steps.src.outputs.path }}/build-scripts/create-android-project.py \ + org.libsdl.testspriteminimal \ + ${{ steps.src.outputs.path }}/test/testspriteminimal.c \ + ${{ steps.src.outputs.path }}/test/icon.h \ + --variant aar \ + --output "/tmp/projects" + echo "path=/tmp/projects/org.libsdl.testspriteminimal" >>$GITHUB_OUTPUT + + echo "" + echo "Project contents:" + echo "" + find "/tmp/projects/org.libsdl.testspriteminimal" + - name: 'Remove SDL sources to make sure they are not used' + run: | + rm -rf "${{ steps.src.outputs.path }}" + - name: 'Copy SDL3 aar into Gradle project' + run: | + cp "${{ github.workspace }}/${{ needs.android.outputs.android-aar }}" "${{ steps.create-gradle-project.outputs.path }}/app/libs" + + echo "" + echo "Project contents:" + echo "" + find "${{ steps.create-gradle-project.outputs.path }}" + - name: 'Build app (Gradle & ndk-build)' + run: | + cd "${{ steps.create-gradle-project.outputs.path }}" + ./gradlew -i assembleRelease -PBUILD_WITH_CMAKE=1 + - name: 'Build app (Gradle & CMake)' + run: | + cd "${{ steps.create-gradle-project.outputs.path }}" + ./gradlew -i assembleRelease diff --git a/android-project/app/jni/CMakeLists.txt b/android-project/app/jni/CMakeLists.txt index 3d49cf343..404b87b37 100644 --- a/android-project/app/jni/CMakeLists.txt +++ b/android-project/app/jni/CMakeLists.txt @@ -2,11 +2,6 @@ cmake_minimum_required(VERSION 3.6) project(GAME) -# armeabi-v7a requires cpufeatures library -# include(AndroidNdkModules) -# android_ndk_import_module_cpufeatures() - - # SDL sources are in a subfolder named "SDL" add_subdirectory(SDL) diff --git a/android-project/app/jni/src/Android.mk b/android-project/app/jni/src/Android.mk index 982f66170..61672d4f6 100644 --- a/android-project/app/jni/src/Android.mk +++ b/android-project/app/jni/src/Android.mk @@ -4,15 +4,16 @@ include $(CLEAR_VARS) LOCAL_MODULE := main -SDL_PATH := ../SDL - -LOCAL_C_INCLUDES := $(LOCAL_PATH)/$(SDL_PATH)/include - # Add your application source files here... -LOCAL_SRC_FILES := YourSourceHere.c +LOCAL_SRC_FILES := \ + YourSourceHere.c + +SDL_PATH := ../SDL # SDL + +LOCAL_C_INCLUDES := $(LOCAL_PATH)/$(SDL_PATH)/include # SDL LOCAL_SHARED_LIBRARIES := SDL3 -LOCAL_LDLIBS := -lGLESv1_CM -lGLESv2 -lOpenSLES -llog -landroid +LOCAL_LDLIBS := -lGLESv1_CM -lGLESv2 -lOpenSLES -llog -landroid # SDL include $(BUILD_SHARED_LIBRARY) diff --git a/build-scripts/androidbuild.sh b/build-scripts/androidbuild.sh deleted file mode 100755 index 399bb3fbd..000000000 --- a/build-scripts/androidbuild.sh +++ /dev/null @@ -1,106 +0,0 @@ -#!/bin/bash - -SOURCES=() -MKSOURCES="" -CURDIR=`pwd -P` - -# Fetch sources -if [[ $# -ge 2 ]]; then - for src in ${@:2} - do - SOURCES+=($src) - MKSOURCES="$MKSOURCES $(basename $src)" - done -else - if [ -n "$1" ]; then - while read src - do - SOURCES+=($src) - MKSOURCES="$MKSOURCES $(basename $src)" - done - fi -fi - -if [ -z "$1" ] || [ -z "$SOURCES" ]; then - echo "Usage: androidbuild.sh com.yourcompany.yourapp < sources.list" - echo "Usage: androidbuild.sh com.yourcompany.yourapp source1.c source2.c ...sourceN.c" - echo "To copy SDL source instead of symlinking: COPYSOURCE=1 androidbuild.sh ... " - exit 1 -fi - -SDLPATH="$( cd "$(dirname "$0")/.." ; pwd -P )" - -if [ -z "$ANDROID_HOME" ];then - echo "Please set the ANDROID_HOME directory to the path of the Android SDK" - exit 1 -fi - -if [ ! -d "$ANDROID_HOME/ndk-bundle" -a -z "$ANDROID_NDK_HOME" ]; then - echo "Please set the ANDROID_NDK_HOME directory to the path of the Android NDK" - exit 1 -fi - -APP="$1" -APPARR=(${APP//./ }) -BUILDPATH="$SDLPATH/build/$APP" - -# Start Building - -rm -rf $BUILDPATH -mkdir -p $BUILDPATH - -cp -r $SDLPATH/android-project/* $BUILDPATH - -# Copy SDL sources -mkdir -p $BUILDPATH/app/jni/SDL -if [ -z "$COPYSOURCE" ]; then - ln -s $SDLPATH/src $BUILDPATH/app/jni/SDL - ln -s $SDLPATH/include $BUILDPATH/app/jni/SDL -else - cp -r $SDLPATH/src $BUILDPATH/app/jni/SDL - cp -r $SDLPATH/include $BUILDPATH/app/jni/SDL -fi - -cp -r $SDLPATH/LICENSE.txt $BUILDPATH/app/jni/SDL -cp -r $SDLPATH/README.md $BUILDPATH/app/jni/SDL -cp -r $SDLPATH/Android.mk $BUILDPATH/app/jni/SDL -cp -r $SDLPATH/CMakeLists.txt $BUILDPATH/app/jni/SDL -cp -r $SDLPATH/cmake $BUILDPATH/app/jni/SDL -sed -i -e "s|YourSourceHere.c|$MKSOURCES|g" $BUILDPATH/app/jni/src/Android.mk -sed -i -e "s|YourSourceHere.c|$MKSOURCES|g" $BUILDPATH/app/jni/src/CMakeLists.txt -sed -i -e "s|org\.libsdl\.app|$APP|g" $BUILDPATH/app/build.gradle -sed -i -e "s|org\.libsdl\.app|$APP|g" $BUILDPATH/app/src/main/AndroidManifest.xml - -# Copy user sources -for src in "${SOURCES[@]}" -do - cp $src $BUILDPATH/app/jni/src -done - -# Create an inherited Activity -cd $BUILDPATH/app/src/main/java -for folder in "${APPARR[@]}" -do - mkdir -p $folder - cd $folder -done - -# Uppercase the first char in the activity class name because it's Java -ACTIVITY="$(echo $folder | awk '{$1=toupper(substr($1,0,1))substr($1,2)}1')Activity" -sed -i -e "s|\"SDLActivity\"|\"$ACTIVITY\"|g" $BUILDPATH/app/src/main/AndroidManifest.xml - -# Fill in a default Activity -cat >"$ACTIVITY.java" <<__EOF__ -package $APP; - -import org.libsdl.app.SDLActivity; - -public class $ACTIVITY extends SDLActivity -{ -} -__EOF__ - -# Update project and build -echo "To build and install to a device for testing, run the following:" -echo "cd $BUILDPATH" -echo "./gradlew installDebug" diff --git a/build-scripts/build-release.py b/build-scripts/build-release.py index 9615f90f6..c9cb82171 100755 --- a/build-scripts/build-release.py +++ b/build-scripts/build-release.py @@ -694,7 +694,7 @@ class Releaser: zip_object.write(test_library, arcname=f"prefab/modules/{self.project}_test/libs/android.{android_abi}/lib{self.project}_test.a") zip_object.writestr(f"prefab/modules/{self.project}_test/libs/android.{android_abi}/abi.json", self.get_prefab_abi_json_text(abi=android_abi, cpp=False, shared=False)) - self.artifacts[f"android-prefab-aar"] = aar_path + self.artifacts[f"android-aar"] = aar_path @classmethod def extract_sdl_version(cls, root: Path, project: str): diff --git a/build-scripts/create-android-project.py b/build-scripts/create-android-project.py new file mode 100755 index 000000000..5ab6f4086 --- /dev/null +++ b/build-scripts/create-android-project.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python +import os +from argparse import ArgumentParser +from pathlib import Path +import re +import shutil +import sys +import textwrap + + +SDL_ROOT = Path(__file__).resolve().parents[1] + +def extract_sdl_version(): + """ + Extract SDL version from SDL3/SDL_version.h + """ + + with open(SDL_ROOT / "include/SDL3/SDL_version.h") as f: + data = f.read() + + major = int(next(re.finditer(r"#define\s+SDL_MAJOR_VERSION\s+([0-9]+)", data)).group(1)) + minor = int(next(re.finditer(r"#define\s+SDL_MINOR_VERSION\s+([0-9]+)", data)).group(1)) + micro = int(next(re.finditer(r"#define\s+SDL_MICRO_VERSION\s+([0-9]+)", data)).group(1)) + return f"{major}.{minor}.{micro}" + +def replace_in_file(path, regex_what, replace_with): + with open(path, "r") as f: + data = f.read() + + new_data, count = re.subn(regex_what, replace_with, data) + + assert count > 0, f"\"{regex_what}\" did not match anything in \"{path}\"" + + with open(path, "w") as f: + f.write(new_data) + + +def android_mk_use_prefab(path): + """ + Replace relative SDL inclusion with dependency on prefab package + """ + + with open(path) as f: + data = "".join(line for line in f.readlines() if "# SDL" not in line) + + data, _ = re.subn("[\n]{3,}", "\n\n", data) + + newdata = data + textwrap.dedent(""" + # https://google.github.io/prefab/build-systems.html + + # Add the prefab modules to the import path. + $(call import-add-path,/out) + + # Import SDL3 so we can depend on it. + $(call import-module,prefab/SDL3) + """) + + with open(path, "w") as f: + f.write(newdata) + +def cmake_mk_no_sdl(path): + """ + Don't add the source directories of SDL/SDL_image/SDL_mixer/... + """ + + with open(path) as f: + lines = f.readlines() + + newlines = [] + for line in lines: + if "add_subdirectory(SDL" in line: + while newlines[-1].startswith("#"): + newlines = newlines[:-1] + continue + newlines.append(line) + + newdata, _ = re.subn("[\n]{3,}", "\n\n", "".join(newlines)) + + with open(path, "w") as f: + f.write(newdata) + +def gradle_add_prefab_and_aar(path, aar): + with open(path) as f: + data = f.read() + + data, count = re.subn("android {", textwrap.dedent(""" + android { + buildFeatures { + prefab true + }"""), data) + assert count == 1 + + data, count = re.subn("dependencies {", textwrap.dedent(f""" + dependencies {{ + implementation files('libs/{aar}')"""), data) + assert count == 1 + + with open(path, "w") as f: + f.write(data) + + +def main(): + description = "Create a simple Android gradle project from input sources." + epilog = "You need to manually copy a prebuilt SDL3 Android archive into the project tree when using the aar variant." + parser = ArgumentParser(description=description, allow_abbrev=False) + parser.add_argument("package_name", metavar="PACKAGENAME", help="Android package name e.g. com.yourcompany.yourapp") + parser.add_argument("sources", metavar="SOURCE", nargs="*", help="Source code of your application. The files are copied to the output directory.") + 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)") + parser.add_argument("--output", "-o", default=SDL_ROOT / "build", type=Path, help="Location where to store the Android project") + parser.add_argument("--version", default=None, help="SDL3 version to use as aar dependency (only used for aar variant)") + + args = parser.parse_args() + if not args.sources: + print("Reading source file paths from stdin (press CTRL+D to stop)") + args.sources = [path for path in sys.stdin.read().strip().split() if path] + if not args.sources: + parser.error("No sources passed") + + if not os.getenv("ANDROID_HOME"): + print("WARNING: ANDROID_HOME environment variable not set", file=sys.stderr) + if not os.getenv("ANDROID_NDK_HOME"): + print("WARNING: ANDROID_NDK_HOME environment variable not set", file=sys.stderr) + + args.sources = [Path(src) for src in args.sources] + + build_path = args.output / args.package_name + + # Remove the destination folder + shutil.rmtree(build_path, ignore_errors=True) + + # Copy the Android project + shutil.copytree(SDL_ROOT / "android-project", build_path) + + # Add the source files to the ndk-build and cmake projects + replace_in_file(build_path / "app/jni/src/Android.mk", r"YourSourceHere\.c", " \\\n ".join(src.name for src in args.sources)) + replace_in_file(build_path / "app/jni/src/CMakeLists.txt", r"YourSourceHere\.c", "\n ".join(src.name for src in args.sources)) + + # Remove placeholder source "YourSourceHere.c" + (build_path / "app/jni/src/YourSourceHere.c").unlink() + + # Copy sources to output folder + for src in args.sources: + if not src.is_file(): + parser.error(f"\"{src}\" is not a file") + shutil.copyfile(src, build_path / "app/jni/src" / src.name) + + sdl_project_files = ( + SDL_ROOT / "src", + SDL_ROOT / "include", + SDL_ROOT / "LICENSE.txt", + SDL_ROOT / "README.md", + SDL_ROOT / "Android.mk", + SDL_ROOT / "CMakeLists.txt", + SDL_ROOT / "cmake", + ) + if args.variant == "copy": + (build_path / "app/jni/SDL").mkdir(exist_ok=True, parents=True) + for sdl_project_file in sdl_project_files: + # Copy SDL project files and directories + if sdl_project_file.is_dir(): + shutil.copytree(sdl_project_file, build_path / "app/jni/SDL" / sdl_project_file.name) + elif sdl_project_file.is_file(): + shutil.copyfile(sdl_project_file, build_path / "app/jni/SDL" / sdl_project_file.name) + elif args.variant == "symlink": + (build_path / "app/jni/SDL").mkdir(exist_ok=True, parents=True) + # Create symbolic links for all SDL project files + for sdl_project_file in sdl_project_files: + os.symlink(sdl_project_file, build_path / "app/jni/SDL" / sdl_project_file.name) + elif args.variant == "aar": + if not args.version: + args.version = extract_sdl_version() + + major = args.version.split(".")[0] + aar = f"SDL{ major }-{ args.version }.aar" + + # Remove all SDL java classes + shutil.rmtree(build_path / "app/src/main/java") + + # Use prefab to generate include-able files + gradle_add_prefab_and_aar(build_path / "app/build.gradle", aar=aar) + + # Make sure to use the prefab-generated files and not SDL sources + android_mk_use_prefab(build_path / "app/jni/src/Android.mk") + cmake_mk_no_sdl(build_path / "app/jni/CMakeLists.txt") + + aar_libs_folder = build_path / "app/libs" + aar_libs_folder.mkdir(parents=True) + with (aar_libs_folder / "copy-sdl-aars-here.txt").open("w") as f: + f.write(f"Copy {aar} to this folder.\n") + + print(f"WARNING: copy { aar } to { aar_libs_folder }", file=sys.stderr) + + # Create entry activity, subclassing SDLActivity + activity = args.package_name[args.package_name.rfind(".") + 1:].capitalize() + "Activity" + activity_path = build_path / "app/src/main/java" / args.package_name.replace(".", "/") / f"{activity}.java" + activity_path.parent.mkdir(parents=True) + with activity_path.open("w") as f: + f.write(textwrap.dedent(f""" + package {args.package_name}; + + import org.libsdl.app.SDLActivity; + + public class {activity} extends SDLActivity + {{ + }} + """)) + + # Add the just-generated activity to the Android manifest + replace_in_file(build_path / "app/src/main/AndroidManifest.xml", "SDLActivity", activity) + + # Update project and build + print("To build and install to a device for testing, run the following:") + print(f"cd {build_path}") + print("./gradlew installDebug") + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/docs/README-android.md b/docs/README-android.md index 2f728f5a6..cb46c604f 100644 --- a/docs/README-android.md +++ b/docs/README-android.md @@ -40,19 +40,19 @@ src/core/android/SDL_android.c Building an app ================================================================================ -For simple projects you can use the script located at build-scripts/androidbuild.sh +For simple projects you can use the script located at build-scripts/create-android-project.py There's two ways of using it: - androidbuild.sh com.yourcompany.yourapp < sources.list - androidbuild.sh com.yourcompany.yourapp source1.c source2.c ...sourceN.c + ./create-android-project.py com.yourcompany.yourapp < sources.list + ./create-android-project.py com.yourcompany.yourapp source1.c source2.c ...sourceN.c sources.list should be a text file with a source file name in each line Filenames should be specified relative to the current directory, for example if you are in the build-scripts directory and want to create the testgles.c test, you'll run: - ./androidbuild.sh org.libsdl.testgles ../test/testgles.c + ./create-android-project.py org.libsdl.testgles ../test/testgles.c One limitation of this script is that all sources provided will be aggregated into a single directory, thus all your source files should have a unique name. @@ -61,7 +61,7 @@ Once the project is complete the script will tell you where the debug APK is loc If you want to create a signed release APK, you can use the project created by this utility to generate it. -Finally, a word of caution: re running androidbuild.sh wipes any changes you may have +Finally, a word of caution: re running create-android-project.py wipes any changes you may have done in the build directory for the app!