diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index 7a8926efd6..1c8c95724d 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -5082,7 +5082,7 @@ if (PkgSkip("SPEEDTREE")==0): # DIRECTORY: panda/src/testbed/ # -if (not RTDIST and not RUNTIME and PkgSkip("PVIEW")==0 and GetTarget() != 'android'): +if (not RTDIST and not RUNTIME and PkgSkip("PVIEW")==0): OPTS=['DIR:panda/src/testbed'] TargetAdd('pview_pview.obj', opts=OPTS, input='pview.cxx') TargetAdd('pview.exe', input='pview_pview.obj') @@ -5101,6 +5101,7 @@ if (not RUNTIME and GetTarget() == 'android'): TargetAdd('org/panda3d/android/NativeIStream.class', opts=OPTS, input='NativeIStream.java') TargetAdd('org/panda3d/android/NativeOStream.class', opts=OPTS, input='NativeOStream.java') TargetAdd('org/panda3d/android/PandaActivity.class', opts=OPTS, input='PandaActivity.java') + TargetAdd('org/panda3d/android/PythonActivity.class', opts=OPTS, input='PythonActivity.java') TargetAdd('p3android_composite1.obj', opts=OPTS, input='p3android_composite1.cxx') TargetAdd('libp3android.dll', input='p3android_composite1.obj') @@ -5111,10 +5112,10 @@ if (not RUNTIME and GetTarget() == 'android'): TargetAdd('android_main.obj', opts=OPTS, input='android_main.cxx') if (not RTDIST and PkgSkip("PVIEW")==0): - TargetAdd('pview_pview.obj', opts=OPTS, input='pview.cxx') + TargetAdd('libpview_pview.obj', opts=OPTS, input='pview.cxx') TargetAdd('libpview.dll', input='android_native_app_glue.obj') TargetAdd('libpview.dll', input='android_main.obj') - TargetAdd('libpview.dll', input='pview_pview.obj') + TargetAdd('libpview.dll', input='libpview_pview.obj') TargetAdd('libpview.dll', input='libp3framework.dll') if not PkgSkip("EGG"): TargetAdd('libpview.dll', input='libpandaegg.dll') @@ -5122,6 +5123,17 @@ if (not RUNTIME and GetTarget() == 'android'): TargetAdd('libpview.dll', input=COMMON_PANDA_LIBS) TargetAdd('libpview.dll', opts=['MODULE', 'ANDROID']) + if (not RTDIST and PkgSkip("PYTHON")==0): + OPTS += ['PYTHON'] + TargetAdd('ppython_ppython.obj', opts=OPTS, input='python_main.cxx') + TargetAdd('libppython.dll', input='android_native_app_glue.obj') + TargetAdd('libppython.dll', input='android_main.obj') + TargetAdd('libppython.dll', input='ppython_ppython.obj') + TargetAdd('libppython.dll', input='libp3framework.dll') + TargetAdd('libppython.dll', input='libp3android.dll') + TargetAdd('libppython.dll', input=COMMON_PANDA_LIBS) + TargetAdd('libppython.dll', opts=['MODULE', 'ANDROID', 'PYTHON']) + # # DIRECTORY: panda/src/androiddisplay/ # @@ -7505,7 +7517,7 @@ def MakeInstallerAndroid(): continue if '.so.' in line: dep = line.rpartition('.so.')[0] + '.so' - oscmd("patchelf --replace-needed %s %s %s" % (line, dep, target)) + oscmd("patchelf --replace-needed %s %s %s" % (line, dep, target), True) else: dep = line @@ -7516,6 +7528,7 @@ def MakeInstallerAndroid(): copy_library(os.path.realpath(fulldep), dep) break + # Now copy every lib in the lib dir, and its dependencies. for base in os.listdir(source_dir): if not base.startswith('lib'): continue @@ -7527,6 +7540,59 @@ def MakeInstallerAndroid(): continue copy_library(source, base) + # Same for Python extension modules. However, Android is strict about + # library naming, so we have a special naming scheme for these, in + # conjunction with a custom import hook to find these modules. + if not PkgSkip("PYTHON"): + suffix = GetExtensionSuffix() + source_dir = os.path.join(GetOutputDir(), "panda3d") + for base in os.listdir(source_dir): + if not base.endswith(suffix): + continue + modname = base[:-len(suffix)] + source = os.path.join(source_dir, base) + copy_library(source, "libpy.panda3d.{}.so".format(modname)) + + # Same for standard Python modules. + import _ctypes + source_dir = os.path.dirname(_ctypes.__file__) + for base in os.listdir(source_dir): + if not base.endswith('.so'): + continue + modname = base.partition('.')[0] + source = os.path.join(source_dir, base) + copy_library(source, "libpy.{}.so".format(modname)) + + def copy_python_tree(source_root, target_root): + for source_dir, dirs, files in os.walk(source_root): + if 'site-packages' in dirs: + dirs.remove('site-packages') + + if not any(base.endswith('.py') for base in files): + continue + + target_dir = os.path.join(target_root, os.path.relpath(source_dir, source_root)) + target_dir = os.path.normpath(target_dir) + os.makedirs(target_dir, 0o755) + + for base in files: + if base.endswith('.py'): + target = os.path.join(target_dir, base) + shutil.copy(os.path.join(source_dir, base), target) + + # Copy the Python standard library to the .apk as well. + from distutils.sysconfig import get_python_lib + stdlib_source = get_python_lib(False, True) + stdlib_target = os.path.join("apkroot", "lib", "python{0}.{1}".format(*sys.version_info)) + copy_python_tree(stdlib_source, stdlib_target) + + # But also copy over our custom site.py. + shutil.copy("panda/src/android/site.py", os.path.join(stdlib_target, "site.py")) + + # And now make a site-packages directory containing our direct/panda3d/pandac modules. + for tree in "panda3d", "direct", "pandac": + copy_python_tree(os.path.join(GetOutputDir(), tree), os.path.join(stdlib_target, "site-packages", tree)) + # Copy the models and config files to the virtual assets filesystem. oscmd("mkdir apkroot/assets") oscmd("cp -R %s apkroot/assets/models" % (os.path.join(GetOutputDir(), "models"))) @@ -7545,7 +7611,11 @@ def MakeInstallerAndroid(): oscmd(aapt_cmd) # And add all the libraries to it. - oscmd("cd apkroot && aapt add ../%s classes.dex lib/%s/lib*.so" % (apk_unaligned, SDK["ANDROID_ABI"])) + oscmd("cd apkroot && aapt add ../%s classes.dex" % (apk_unaligned)) + for path, dirs, files in os.walk('apkroot/lib'): + if files: + rel = os.path.relpath(path, 'apkroot') + oscmd("cd apkroot && aapt add ../%s %s/*" % (apk_unaligned, rel)) # Now align the .apk, which is necessary for Android to load it. oscmd("zipalign -v -p 4 %s %s" % (apk_unaligned, apk_unsigned)) diff --git a/panda/src/android/PythonActivity.java b/panda/src/android/PythonActivity.java new file mode 100644 index 0000000000..0d282d84a5 --- /dev/null +++ b/panda/src/android/PythonActivity.java @@ -0,0 +1,23 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file PythonActivity.java + * @author rdb + * @date 2018-02-04 + */ + +package org.panda3d.android; + +import org.panda3d.android.PandaActivity; + +/** + * This is only declared as a separate class from PandaActivity so that we + * can have two separate activity definitions in ApplicationManifest.xml. + */ +public class PythonActivity extends PandaActivity { +} diff --git a/panda/src/android/pview_manifest.xml b/panda/src/android/pview_manifest.xml index 1560bdd828..b462e4018a 100644 --- a/panda/src/android/pview_manifest.xml +++ b/panda/src/android/pview_manifest.xml @@ -45,13 +45,38 @@ - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/panda/src/android/python_main.cxx b/panda/src/android/python_main.cxx new file mode 100644 index 0000000000..c1fc0a39fe --- /dev/null +++ b/panda/src/android/python_main.cxx @@ -0,0 +1,80 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file python_main.cxx + * @author rdb + * @date 2018-02-12 + */ + +#include "dtoolbase.h" +#include "config_android.h" +#include "executionEnvironment.h" + +#undef _POSIX_C_SOURCE +#undef _XOPEN_SOURCE +#include +#if PY_MAJOR_VERSION >= 3 +#include +#endif + +#include + +/** + * The main entry point for the Python activity. Called by android_main. + */ +int main(int argc, char *argv[]) { + if (argc <= 1) { + return 1; + } + + // Help out Python by telling it which encoding to use + Py_FileSystemDefaultEncoding = "utf-8"; + + Py_SetProgramName(Py_DecodeLocale("ppython", nullptr)); + + // Set PYTHONHOME to the location of the .apk file. + string apk_path = ExecutionEnvironment::get_binary_name(); + Py_SetPythonHome(Py_DecodeLocale(apk_path.c_str(), nullptr)); + + // We need to make zlib available to zipimport, but I don't know how + // we could inject our import hook before Py_Initialize, so instead + // load it as though it were a built-in module. + void *zlib = dlopen("libpy.zlib.so", RTLD_NOW); + if (zlib != nullptr) { + void *init = dlsym(zlib, "PyInit_zlib"); + if (init != nullptr) { + PyImport_AppendInittab("zlib", (PyObject *(*)())init); + } + } + + Py_Initialize(); + + // This is used by the import hook to locate the module libraries. + Filename dtool_name = ExecutionEnvironment::get_dtool_name(); + string native_dir = dtool_name.get_dirname(); + PyObject *py_native_dir = PyUnicode_FromStringAndSize(native_dir.c_str(), native_dir.size()); + PySys_SetObject("_native_library_dir", py_native_dir); + Py_DECREF(py_native_dir); + + int sts = 1; + FILE *fp = fopen(argv[1], "r"); + if (fp != nullptr) { + int res = PyRun_AnyFile(fp, argv[1]); + if (res > 0) { + sts = 0; + } else { + android_cat.error() << "Error running " << argv[1] << "\n"; + PyErr_Print(); + } + } else { + android_cat.error() << "Unable to open " << argv[1] << "\n"; + } + + Py_Finalize(); + return sts; +} diff --git a/panda/src/android/run_pview.sh b/panda/src/android/run_pview.sh new file mode 100755 index 0000000000..6f3b6f1476 --- /dev/null +++ b/panda/src/android/run_pview.sh @@ -0,0 +1,14 @@ +# This script can be used for launching the Panda viewer from the Android +# terminal environment, for example from within termux. It uses a socket +# to pipe the command-line output back to the terminal. + +port=12345 + +if [[ $# -eq 0 ]] ; then + echo "Pass full path of model" + exit 1 +fi + +am start --activity-clear-task -n org.panda3d.sdk/org.panda3d.android.PandaActivity --user 0 --es org.panda3d.OUTPUT_URI tcp://127.0.0.1:$port --grant-read-uri-permission --grant-write-uri-permission file://$(realpath $1) + +nc -l -p $port diff --git a/panda/src/android/run_python.sh b/panda/src/android/run_python.sh new file mode 100755 index 0000000000..9a8adc710b --- /dev/null +++ b/panda/src/android/run_python.sh @@ -0,0 +1,14 @@ +# This script can be used for launching a Python script from the Android +# terminal environment, for example from within termux. It uses a socket +# to pipe the command-line output back to the terminal. + +port=12345 + +if [[ $# -eq 0 ]] ; then + echo "Pass full path of script" + exit 1 +fi + +am start --activity-clear-task -n org.panda3d.sdk/org.panda3d.android.PythonActivity --user 0 --es org.panda3d.OUTPUT_URI tcp://127.0.0.1:$port --grant-read-uri-permission --grant-write-uri-permission file://$(realpath $1) + +nc -l -p $port diff --git a/panda/src/android/site.py b/panda/src/android/site.py new file mode 100644 index 0000000000..fd3909ede8 --- /dev/null +++ b/panda/src/android/site.py @@ -0,0 +1,34 @@ +import sys +import os + +from importlib.abc import Loader, MetaPathFinder +from importlib.machinery import ModuleSpec + +if sys.version_info >= (3, 5): + from importlib import _bootstrap_external +else: + from importlib import _bootstrap as _bootstrap_external + +sys.platform = "android" + +class AndroidExtensionFinder(MetaPathFinder): + @classmethod + def find_spec(cls, fullname, path=None, target=None): + soname = 'libpy.' + fullname + '.so' + path = os.path.join(sys._native_library_dir, soname) + + if os.path.exists(path): + loader = _bootstrap_external.ExtensionFileLoader(fullname, path) + return ModuleSpec(fullname, loader, origin=path) + + +def main(): + """Adds the site-packages directory to the sys.path. + Also, registers the import hook for extension modules.""" + + sys.path.append('{0}/lib/python{1}.{2}/site-packages'.format(sys.prefix, *sys.version_info)) + sys.meta_path.append(AndroidExtensionFinder) + + +if not sys.flags.no_site: + main()