Explorar el Código

Implement 3rd-party modules support for new CMake build system.

Existing 3rd-party module with Android.mk will be converted to CMake, with caveats
of course. Read lua-modules-readme.txt for more info!

Fixes #241.
Miku AuahDark hace 3 años
padre
commit
d55d3ce715

+ 36 - 1
app/build.gradle

@@ -18,11 +18,45 @@ android {
                 // Transitive shared library dependency is not taken into account, this
                 // will cause liboboe.so not get included. love depends on OpenAL that
                 // depends on oboe::oboe. So, add "OpenAL" target.
-                targets "love", "OpenAL"
+                targets "love_android"
             }
         }
     }
 
+    def retrieveAll3pModules = { ->
+        def modules = []
+
+        fileTree("src/main/cpp/lua-modules/").visit { FileVisitDetails details ->
+            if (details.isDirectory()) {
+                if (file(details.file.path + "/Android.mk").exists() ||
+                        file(details.file.path + "/CMakeLists.mk").exists()) {
+                    logger.lifecycle("3rd-party module: " + details.file.path)
+
+                    def javainfo = file(details.file.path + "/java.txt")
+                    if (javainfo.exists()) {
+                        def fstream = new FileInputStream(javainfo)
+                        def infile = new BufferedReader(new InputStreamReader(fstream))
+                        def javapath = infile.readLine().replace("\\", "/")
+                        def mpath = null
+
+                        if (javapath[0] != '/') {
+                            mpath = details.file.path + "/" + javapath
+                        } else {
+                            mpath = details.file.path + javapath
+                        }
+
+                        modules << mpath
+
+                        logger.lifecycle("Registered path " + mpath)
+                        infile.close()
+                    }
+                }
+            }
+        }
+
+        return modules
+    }
+
     buildTypes {
         release {
             minifyEnabled false
@@ -56,6 +90,7 @@ android {
                 srcDir 'src/main/cpp/megasource/libs/SDL2/android-project/app/src/main/java'
                 srcDir 'src/main/java'
                 srcDir 'src/main/cpp/love/src/libraries/luahttps/src/android/java'
+                srcDirs += retrieveAll3pModules()
             }
         }
         normal {

+ 73 - 1
app/src/main/cpp/CMakeLists.txt

@@ -1,7 +1,79 @@
+#
+# Copyright (c) 2006-2022 LOVE Development Team
+#
+# This software is provided 'as-is', without any express or implied
+# warranty.  In no event will the authors be held liable for any damages
+# arising from the use of this software.
+#
+# Permission is granted to anyone to use this software for any purpose,
+# including commercial applications, and to alter it and redistribute it
+# freely, subject to the following restrictions:
+#
+# 1. The origin of this software must not be misrepresented; you must not
+#    claim that you wrote the original software. If you use this software
+#    in a product, an acknowledgment in the product documentation would be
+#    appreciated but is not required.
+# 2. Altered source versions must be plainly marked as such, and must not be
+#    misrepresented as being the original software.
+# 3. This notice may not be removed or altered from any source distribution.
+#
+
 cmake_minimum_required(VERSION 3.21)
 
 project(love-android LANGUAGES C CXX)
 
-set(MEGA_LOVE "${CMAKE_CURRENT_SOURCE_DIR}/love")
+add_custom_target(love_android)
+set(LOVE_ANDROID 1)
 
+# Include Megasource and LOVE
+set(MEGA_LOVE "${CMAKE_CURRENT_SOURCE_DIR}/love")
 add_subdirectory(megasource)
+
+add_dependencies(love_android love OpenAL)
+
+find_package(Python 3.9 COMPONENTS Interpreter)
+if(NOT Python_Interpreter_FOUND)
+	message(WARNING "No Python detected, Android.mk won't be converted to CMakeLists.txt!")
+endif()
+
+# List lua-modules
+
+file(GLOB MODULE_LIST
+	LIST_DIRECTORIES TRUE
+	RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}"
+	CONFIGURE_DEPENDS
+	"lua-modules/*"
+)
+foreach(dir ${MODULE_LIST})
+	if(IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/${dir}")
+		set(has_run_cmake 0)
+		# Load either CMakeLists.txt or convert Android.mk to CMakeLists.txt
+		if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${dir}/CMakeLists.txt")
+			add_subdirectory(${dir})
+			set(has_run_cmake 1)
+		elseif(Python_Interpreter_FOUND AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${dir}/Android.mk")
+			get_filename_component(item_sanitized0 "${dir}" NAME)
+			string(REGEX REPLACE "[^\\w-]" "-" item_sanitized1 "${item_sanitized0}")
+			string(REGEX REPLACE "-+" "-" item "${item_sanitized0}")
+			message(STATUS "Converting ${dir} Android.mk to CMakeLists.txt")
+			execute_process(RESULT_VARIABLE status
+				COMMAND "${Python_EXECUTABLE}" amkparse.py convert "${item}" "${dir}/Android.mk"
+				WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
+				OUTPUT_FILE "${dir}/CMakeLists.txt"
+			)
+			if("${status}" STREQUAL "0")
+				add_subdirectory(${dir})
+			set(has_run_cmake 1)
+			else()
+				message(FATAL_ERROR "amkparse.py returned ${status}")
+			endif()
+		endif()
+
+		# If CMake has been run, retrieve targets and add it as dependency
+		if(has_run_cmake)
+			get_property(defined_build_targets DIRECTORY "${dir}" PROPERTY BUILDSYSTEM_TARGETS)
+			get_property(defined_import_targets DIRECTORY "${dir}" PROPERTY IMPORTED_TARGETS)
+			add_dependencies(love_android ${defined_build_targets} ${defined_import_targets})
+		endif()
+	endif()
+endforeach()

+ 464 - 0
app/src/main/cpp/amkparse.py

@@ -0,0 +1,464 @@
+import io
+import json
+import os
+import re
+import sys
+
+
+P_COMMENTS = re.compile(r"(?<!\\)#.+")
+P_CONTINUATION = re.compile(r"\s*\\\r?\n\s*")
+P_EMPTY_LINE = re.compile(r"\s*[\r?\n]+\s*")
+P_VAR_ASSIGN = re.compile(r"([a-zA-Z_]\w*)\s*:?:?=\"?\s*(.*)\"?")
+P_MULTI_WHITESPACE = re.compile(r"\s+")
+P_CLEAN_VARS = re.compile(r"include\s*\$[\(\{]CLEAR_VARS[\)\}]\s*", re.MULTILINE)
+P_VARIABLE_REF = re.compile(r"\$[\(\{]([a-zA-Z_]\w*)[\)\}]")
+
+P_BUILD_TYPES = {
+    "executable": re.compile(r"include\s*\$[\(\{]BUILD_EXECUTABLE[\)\}]\s*", re.MULTILINE),
+    "shared": re.compile(r"include\s*\$[\(\{]BUILD_SHARED_LIBRARY[\)\}]\s*", re.MULTILINE),
+    "static": re.compile(r"include\s*\$[\(\{]BUILD_STATIC_LIBRARY[\)\}]\s*", re.MULTILINE),
+    "prebuilt_shared": re.compile(r"include\s*\$[\(\{]PREBUILT_SHARED_LIBRARY[\)\}]\s*", re.MULTILINE),
+    "prebuilt_static": re.compile(r"include\s*\$[\(\{]PREBUILT_STATIC_LIBRARY[\)\}]\s*", re.MULTILINE),
+}
+
+substitute_love_with_luajit = True
+
+
+def transform_makefile(mk: str):
+    # Remove comments
+    mk = re.sub(P_COMMENTS, "", mk)
+    # Remove multiline declaration
+    mk = re.sub(P_CONTINUATION, " ", mk)
+    # Remove multiple newlines
+    mk = re.sub(P_EMPTY_LINE, "\n", mk)
+    mk = re.sub(P_EMPTY_LINE, "\n", mk)
+    # Simplify assignment
+    mk = re.sub(P_VAR_ASSIGN, r"\1=\2", mk)
+    return mk
+
+
+def list_if_needed(data):
+    if isinstance(data, str):
+        return [data]
+    return data
+
+
+def str_if_needed(data):
+    if isinstance(data, list):
+        return " ".join(data)
+    return data
+
+
+def expand_value(value: str, variables: dict[str, str]):
+    def do_replace(x: re.Match[str]):
+        v = x.group(1)
+        return str_if_needed(variables.get(v, f"$({v})"))
+
+    return re.sub(P_VARIABLE_REF, do_replace, value)
+
+
+def extract_variables(mk: str, variables: dict[str, str] = None):
+    result = {}
+    for match in re.finditer(P_VAR_ASSIGN, mk):
+        value = match.group(2).strip()
+        # Try convert to boolean
+        if value == "true" or value == "false":
+            value = value == "true"
+        # Is it array of strings?
+        elif value.find(" ") >= 0:
+            if variables:
+                # Expand first
+                value = expand_value(value, variables)
+            value = re.sub(P_MULTI_WHITESPACE, " ", value).split(" ")
+        else:
+            try:
+                # Try int conversion
+                value = int(value)
+            except ValueError:
+                try:
+                    # Try float conversion
+                    value = float(value)
+                except ValueError:
+                    # Fallback default (str)
+                    if variables:
+                        value = expand_value(value, variables)
+        result[match.group(1)] = value
+    return result
+
+
+def separate_makefiles(mk: str):
+    # Normalize "include $(CLEAR_VARS)"
+    result = re.sub(P_CLEAN_VARS, "include $(CLEAN_VARS)", mk).split("include $(CLEAN_VARS)")
+    # Transform makefile
+    result = map(transform_makefile, result)
+    # Remove "LOCAL_PATH"
+    def test_filt(x):
+        return x.find("LOCAL_PATH=") == -1
+
+    result = filter(test_filt, result)
+    return list(result)
+
+
+def extract_variables_with_expand(mk: str, target_vars: dict[str, str]):
+    variables = extract_variables(mk, target_vars)
+    variables = extract_variables(mk, variables | target_vars)
+    return variables
+
+
+def determine_build_type(mk: str):
+    for name, pattern in P_BUILD_TYPES.items():
+        if re.search(pattern, mk):
+            return name
+    return "unknown"
+
+
+class CFlagsData:
+    def __init__(self, cflags: str):
+        # Variable setup
+        self.standard = None
+        self.include_paths = []
+        self.features = set()
+        self.defines = set()
+        self.other = []
+        # Parse
+        for flag in cflags.split():
+            if flag.startswith("-std="):
+                # std
+                value = flag[5:].replace("gnu++", "c++")
+                if value == "c99":
+                    self.standard = 99
+                elif value == "c++11" or value == "c++0x" or value == "c11":
+                    self.standard = 11
+                elif value == "c++14":
+                    self.standard = 14
+                elif value == "c++17" or value == "c17":
+                    self.standard = 17
+                elif value == "c++2a" or value == "c++20":
+                    self.standard = 20
+                else:
+                    print(f"Unknown -std: {value}", file=sys.stderr)
+            elif flag.startswith("-I"):
+                # Include paths
+                self.include_paths.append(flag[2:])
+            elif flag.startswith("-f"):
+                # Features
+                self.features.add(flag[2:])
+            elif flag.startswith("-D"):
+                # Defines
+                self.defines.add(flag[2:])
+            else:
+                self.other.append(flag)
+
+    @property
+    def unknowns(self):
+        return " ".join(self.others)
+
+
+class LDFlagsData:
+    def __init__(self, ldflags: str):
+        # Variable setup
+        self.libraries = set()
+        self.library_paths = []
+        self.other = []
+        # Parse
+        for flag in ldflags.split():
+            if flag.startswith("-fuse-ld="):
+                # linker, ignore
+                pass
+            elif flag.startswith("-l"):
+                # Native, built-in libraries
+                self.libraries.add(flag[2:])
+            elif flag.startswith("-L"):
+                # Library paths
+                self.library_paths.append(flag[2:])
+            else:
+                self.other.append(flag)
+
+    @property
+    def unknowns(self):
+        return " ".join(self.others)
+
+
+def filter_libraries(name: str):
+    if len(name) < 2:
+        return name
+    if name[0] == "-":
+        if name[1] == "l":
+            return name[2:]
+    else:
+        if name.startswith("lib"):
+            return name[3:]
+        return name
+    return ""
+
+
+def get_default_output_file(name: str):
+    return name if name.startswith("lib") else ("lib" + name)
+
+
+class SerializedMakefile:
+    def __init__(self, mk: str, compatsubst: bool = True):
+        variables = extract_variables_with_expand(mk, target_vars)
+        build_type = determine_build_type(mk)
+        # Essential
+        self.name = variables["LOCAL_MODULE"]
+        if self.name.startswith("lib"):
+            self.name = self.name[3:]
+        self.output = variables.get("LOCAL_MODULE_FILENAME", get_default_output_file(self.name))
+        self.type = build_type
+        self.sources = list_if_needed(variables["LOCAL_SRC_FILES"])
+        self.include = list_if_needed(variables.get("LOCAL_C_INCLUDES", []))
+        self.export_include = list_if_needed(variables.get("LOCAL_EXPORT_C_INCLUDES", []))
+        self.allow_undefined = str_if_needed(variables.get("LOCAL_ALLOW_UNDEFINED_SYMBOLS", "")).lower() == "true"
+        self.force_arm_code = str_if_needed(variables.get("LOCAL_ARM_MODE", "")) == "arm"
+        # CFlags
+        cflags = CFlagsData(str_if_needed(variables.get("LOCAL_CFLAGS", "")))
+        export_cflags = CFlagsData(str_if_needed(variables.get("LOCAL_EXPORT_CFLAGS", "")))
+        cppflags = CFlagsData(str_if_needed(variables.get("LOCAL_CPPFLAGS", "")))
+        export_cppflags = CFlagsData(str_if_needed(variables.get("LOCAL_EXPORT_CPPFLAGS", "")))
+        self.include.extend(cflags.include_paths)
+        self.include.extend(cppflags.include_paths)
+        self.export_include.extend(export_cflags.include_paths)
+        self.export_include.extend(export_cppflags.include_paths)
+        # C flags
+        self.c_standard = cflags.standard
+        self.c_compile_features = list(cflags.features)
+        self.c_compile_flags = cflags.other
+        self.export_c_standard = export_cflags.standard
+        self.export_c_compile_features = list(export_cflags.features)
+        self.export_c_compile_flags = export_cflags.other
+        # C++ flags
+        self.cpp_standard = cppflags.standard
+        self.cpp_compile_features = list(cppflags.features)
+        self.cpp_compile_flags = cppflags.other
+        self.export_cpp_standard = export_cppflags.standard
+        self.export_cpp_compile_features = list(export_cppflags.features)
+        self.export_cpp_compile_flags = export_cppflags.other
+        # Linker flags/libraries
+        link_libraries = set()
+        link_libraries.update(
+            filter_libraries(name) for name in list_if_needed(variables.get("LOCAL_SHARED_LIBRARIES", []))
+        )
+        link_libraries.update(
+            filter_libraries(name) for name in list_if_needed(variables.get("LOCAL_STATIC_LIBRARIES", []))
+        )
+        self.link_library_path = []
+        self.link_library_options = []
+        if build_type.find("static") == -1:
+            ldflags = LDFlagsData(str_if_needed(variables.get("LOCAL_LDFLAGS", "")))
+            ldlibs = list_if_needed(variables.get("LOCAL_LDLIBS", []))
+            link_libraries.update(filter_libraries(name) for name in ldlibs)
+            link_libraries.update(filter_libraries(name) for name in ldflags.libraries)
+            self.link_library_path.extend(ldflags.library_paths)
+            self.link_library_options.extend(ldflags.other)
+        self.link_libraries = list(filter(None, link_libraries))
+        export_ldflags = LDFlagsData(str_if_needed(variables.get("LOCAL_EXPORT_LDFLAGS", "")))
+        export_link_libraries = set()
+        export_link_libraries.update(
+            filter_libraries(name) for name in list_if_needed(variables.get("LOCAL_LDLIBS", []))
+        )
+        export_link_libraries.update(filter_libraries(name) for name in export_ldflags.libraries)
+        self.export_link_library_path = export_ldflags.library_paths
+        self.export_link_library_options = export_ldflags.other
+        self.export_link_libraries = list(filter(None, export_link_libraries))
+        if compatsubst:
+            self.link_libraries = list(map(lambda x: "luajit" if x == "love" else x, self.link_libraries))
+            self.export_link_libraries = list(map(lambda x: "luajit" if x == "love" else x, self.export_link_libraries))
+
+    def __iter__(self):
+        yield "name", self.name,
+        yield "output", self.output,
+        yield "type", self.type,
+        yield "allowUndefined", self.allow_undefined
+        yield "forceARMCode", self.force_arm_code
+        yield "sources", self.sources
+        yield "include", self.include
+        yield "exportInclude", self.export_include
+        yield "cStandard", self.c_standard
+        yield "cCompileFeatures", self.c_compile_features
+        yield "cCompileFlags", self.c_compile_flags
+        yield "cppStandard", self.cpp_standard
+        yield "cppCompileFeatures", self.cpp_compile_features
+        yield "cppCompileFlags", self.cpp_compile_flags
+        yield "exportCStandard", self.export_c_standard
+        yield "exportCCompileFeatures", self.export_c_compile_features
+        yield "exportCCompileFlags", self.export_c_compile_flags
+        yield "exportCppStandard", self.export_cpp_standard
+        yield "exportCppCompileFeatures", self.export_cpp_compile_features
+        yield "exportCppCompileFlags", self.export_cpp_compile_flags
+        yield "linkLibraries", self.link_libraries
+        yield "linkLibraryPath", self.link_library_path
+        yield "linkLibraryOptions", self.link_library_options
+        yield "exportLinkLibraries", self.export_link_libraries
+        yield "exportLinkLibraryPath", self.export_link_library_path
+        yield "exportLinkLibraryOptions", self.export_link_library_options
+
+
+def convert_to_serialized(mk: list[str], target_vars: dict[str, str]):
+    result = []  # type: list[SerializedMakefile]
+    for mks in mk:
+        result.append(SerializedMakefile(mks, substitute_love_with_luajit))
+    return result
+
+
+def convert_to_json(mk: list[str], target_vars: dict[str, str]):
+    return json.dumps([dict(x) for x in convert_to_serialized(mk, target_vars)])
+
+
+def write_cmake(stream: io.StringIO, funcname: str, target: str, kind: str = None, props: list[str] = None):
+    stream.write(funcname)
+    stream.write("(")
+    stream.write(target)
+    if kind:
+        stream.write(" ")
+        stream.write(kind)
+    if props:
+        stream.write("\n")
+        for prop in props:
+            stream.write("\t")
+            stream.write(prop)
+            stream.write("\n")
+    stream.write(")\n")
+
+
+def convert_to_cmake(mk: list[str], target_vars: dict[str, str], project: str):
+    serialized_mk = convert_to_serialized(mk, target_vars)
+    result = io.StringIO()
+    result.write("# Generated by amkparse.py\n")
+    result.write("# Modify if needed then remove this comment.\n")
+    result.write("cmake_minimum_required(VERSION 3.22)\n\n")
+    result.write(f"project({project} LANGUAGES C CXX)\n\n")
+    for s in serialized_mk:
+        result.write(f"#\n# {s.name}\n#\n")
+        if s.type.startswith("prebuilt"):
+            # Prebuilt needs either .so or .a
+            kind = "SHARED" if s.type.find("shared") != -1 else "STATIC"
+            write_cmake(result, "add_library", s.name, f"IMPORTED {kind}")
+            write_cmake(result, "set_target_properties", s.name, "PROPERTIES", [f'IMPORTED_LOCATION "{s.sources[0]}"'])
+            if len(s.export_include) > 0:
+                write_cmake(result, "target_include_directories", s.name, "INTERFACE", s.export_include)
+        else:
+            # add_library/add_executable
+            if s.type == "executable":
+                write_cmake(result, "add_executable", s.name, None, s.sources)
+            else:
+                kind = "SHARED" if s.type.find("shared") != -1 else "STATIC"
+                write_cmake(result, "add_library", s.name, kind, s.sources)
+            # set_target_properties
+            c_standard = s.export_c_standard or s.c_standard
+            cpp_standard = s.export_cpp_standard or s.cpp_standard
+            target_props = [f"OUTPUT_NAME {s.output}", 'PREFIX ""']
+            if c_standard:
+                target_props.append(f"C_STANDARD {c_standard}")
+            if cpp_standard:
+                target_props.append(f"CXX_STANDARD {cpp_standard}")
+            write_cmake(result, "set_target_properties", s.name, "PROPERTIES", target_props)
+            # target_include_directories PRIVATE
+            if len(s.include) > 0:
+                write_cmake(result, "target_include_directories", s.name, "PRIVATE", s.include)
+            # target_include_directories PUBLIC
+            if len(s.export_include) > 0:
+                write_cmake(result, "target_include_directories", s.name, "PUBLIC", s.export_include)
+            # target_link_directories PRIVATE
+            if len(s.link_library_path) > 0:
+                write_cmake(result, "target_link_directories", s.name, "PRIVATE", s.link_library_path)
+            # target_link_directories PUBLIC
+            if len(s.export_link_library_path) > 0:
+                write_cmake(result, "target_link_directories", s.name, "PUBLIC", s.export_link_library_path)
+            # target_link_libraries
+            link_libraries = None
+            export_link_libraries = None
+            if s.type.find("static") != -1:
+                # Merge export and non-export
+                export_link_libraries = set()
+                export_link_libraries.update(s.link_libraries)
+                export_link_libraries.update(s.export_link_libraries)
+            else:
+                link_libraries = s.link_libraries
+                export_link_libraries = s.export_link_libraries
+            if link_libraries:
+                write_cmake(result, "target_link_libraries", s.name, "PRIVATE", link_libraries)
+            if export_link_libraries:
+                write_cmake(result, "target_link_libraries", s.name, "PUBLIC", export_link_libraries)
+            if len(s.c_compile_flags) > 0:
+                print("# TODO: compile flags:", *s.c_compile_flags, file=result)
+            if len(s.c_compile_features) > 0:
+                print("# TODO: compile features:", *s.c_compile_features, file=result)
+            if len(s.cpp_compile_flags) > 0:
+                print("# TODO: compile flags cpp:", *s.cpp_compile_flags, file=result)
+            if len(s.cpp_compile_features) > 0:
+                print("# TODO: compile features cpp:", *s.cpp_compile_features, file=result)
+            if len(s.export_c_compile_flags) > 0:
+                print("# TODO: export compile flags:", *s.export_c_compile_flags, file=result)
+            if len(s.export_c_compile_features) > 0:
+                print("# TODO: export compile features:", *s.export_c_compile_features, file=result)
+            if len(s.export_c_compile_flags) > 0:
+                print("# TODO: export compile flags cpp:", *s.export_c_compile_flags, file=result)
+            if len(s.export_c_compile_features) > 0:
+                print("# TODO: export compile features cpp:", *s.export_c_compile_features, file=result)
+            if len(s.link_library_options) > 0:
+                print("# TODO: link options:", *s.link_library_options, file=result)
+            if len(s.export_link_library_options) > 0:
+                print("# TODO: export link options:", *s.export_link_library_options, file=result)
+    return result.getvalue()
+
+
+def create_target_vars(path: str, arch: str, api: str):
+    if arch == "cmake":
+        return {
+            "LOCAL_PATH": "${CMAKE_CURRENT_SOURCE_DIR}",
+            "TARGET_ARCH": "${CMAKE_ANDROID_ARCH}",
+            "TARGET_ARCH_ABI": "${CMAKE_ANDROID_ARCH_ABI}",
+            "TARGET_PLATFORM": "android-${CMAKE_SYSTEM_NAME}",
+            "TARGET_ABI": "android-${CMAKE_SYSTEM_NAME}-${CMAKE_ANDROID_ARCH_ABI}",
+        }
+    if arch == "arm64" or arch == "arm64-v8a":
+        arch_pure = "arm64"
+        arch_abi = "arm64-v8a"
+    elif arch == "armeabi-v7a" or arch == "arm":
+        arch_pure = "arm"
+        arch_abi = "armeabi-v7a"
+    else:
+        arch_pure = arch
+        arch_abi = arch
+    return {
+        "LOCAL_PATH": os.path.dirname(os.path.realpath(path)).replace("\\", "/"),
+        "TARGET_ARCH": arch_pure,
+        "TARGET_ARCH_ABI": arch_abi,
+        "TARGET_PLATFORM": f"android-{api}",
+        "TARGET_ABI": f"android-{api}-{arch_abi}",
+    }
+
+
+if __name__ == "__main__":
+    import argparse
+
+    parser = argparse.ArgumentParser()
+    subparser = parser.add_subparsers(dest="command", required=True)
+    # CMake converter subparser
+    convert_parser = subparser.add_parser("convert")
+    convert_parser.add_argument("--arch", type=str, help="Target architecture", default="cmake")
+    convert_parser.add_argument("--api", type=int, help="Minimum Android API level", default=0)
+    convert_parser.add_argument("--no-compat", action="store_true", help='Don\'t change "love" to "luajit" target.')
+    convert_parser.add_argument("project", type=str, help="Project name")
+    convert_parser.add_argument("input", type=str, help="Android.mk")
+    # JSON dump subparser
+    dump_parser = subparser.add_parser("dump")
+    dump_parser.add_argument("arch", type=str, help="Target architecture")
+    dump_parser.add_argument("api", type=int, help="Minimum Android API level")
+    dump_parser.add_argument("input", type=str, help="Android.mk")
+    # Parse
+    args = parser.parse_args()
+    # Check command
+    if args.command == "convert":
+        if args.no_compat:
+            substitute_love_with_luajit = False
+        target_vars = create_target_vars(args.input, args.arch, args.api)
+        with open(args.input, "r", encoding="UTF-8") as f:
+            projects = separate_makefiles(f.read())
+            print(convert_to_cmake(projects, target_vars, args.project))
+    elif args.command == "dump":
+        target_vars = create_target_vars(args.input, args.arch, args.api)
+        with open(args.input, "r", encoding="UTF-8") as f:
+            projects = separate_makefiles(f.read())
+            print(convert_to_json(projects, target_vars))

+ 21 - 0
app/src/main/cpp/lua-modules/lua-modules-readme.txt

@@ -0,0 +1,21 @@
+This folder should contain external Lua C modules that will be shipped along with LOVE. One for each folder
+
+Each folder must contains:
+* Either CMakeLists.txt or Android.mk.
+  * If Android.mk is present:
+    The Android.mk will be converted to CMakeLists.txt. This conversion may not 100% perfect!
+    The LOCAL_MODULE_FILENAME must be set accordingly as ndk-build appends "lib" prefix from LOCAL_MODULE by default.
+    For Lua includes and libraries, simply link the native library with "liblove".
+    Note: This will convert Android.mk to CMakeLists.txt. The conversion may not 100% perfect. Conditionals are not
+          parsed and treated as if all codepaths are taken. Modify the generated CMakeLists.txt if necessary!
+    Caveat: If the library is meant to be loaded as require("my.library") then you MUST set LOCAL_MODULE_FILENAME to
+            my.library (with dots).
+  * If CMakeLists.txt is present:
+    The CMake script is loaded directly. 
+    For Lua includes and libraries, link with "luajit" target.
+    Caveat: If the library is meant to be loaded as require("my.library") then you MUST set OUTPUT_NAME property to
+            "my.library" (with dots) and PREFIX property to "" (empty string) on your CMake target using
+            "set_target_properties".
+* If your module interacts with Java-side, a file java.txt must contain where it should look Java source files.
+  Example, if you have src/java/your/package/name/MyFile.java, then you need to write "src/java" in java.txt.
+  If this file is absent, then your Java-side code will not be compiled along.