Browse Source

android: create android project in create-android-project.py python script

This script supersedes androidbuild.sh, and also supports using a SDL3 prefab archive
Anonymous Maarten 9 months ago
parent
commit
50ae47af5e

+ 5 - 1
.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 ""

+ 68 - 1
.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

+ 0 - 5
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)
 

+ 6 - 5
android-project/app/jni/src/Android.mk

@@ -4,15 +4,16 @@ include $(CLEAR_VARS)
 
 LOCAL_MODULE := main
 
-SDL_PATH := ../SDL
+# Add your application source files here...
+LOCAL_SRC_FILES := \
+    YourSourceHere.c
 
-LOCAL_C_INCLUDES := $(LOCAL_PATH)/$(SDL_PATH)/include
+SDL_PATH := ../SDL  # SDL
 
-# Add your application source files here...
-LOCAL_SRC_FILES := YourSourceHere.c
+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)

+ 0 - 106
build-scripts/androidbuild.sh

@@ -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"

+ 1 - 1
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):

+ 217 - 0
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())

+ 5 - 5
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!