Skip to content

Commit 00f86c3

Browse files
committed
ffmpeg, python3: include executable (kivy#3276)
* `ffmpeg`, `python3`: include binary * doc link fix
1 parent 6ca4f29 commit 00f86c3

File tree

7 files changed

+495
-65
lines changed

7 files changed

+495
-65
lines changed

doc/source/apis.rst

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,53 @@ https://developer.android.com/reference/android/Manifest.permission
8080
Other common tasks
8181
------------------
8282

83+
Running executables
84+
~~~~~~~~~~~~~~~~~~~
85+
86+
Android restricts executing files from application data directories.
87+
``python-for-android`` works around this by creating symbolic links to a
88+
small set of supported executables inside an internal executable
89+
directory that is added to ``PATH``.
90+
91+
During startup, ``start.c`` (compiled into ``libmain.so``) scans loaded
92+
native libraries and creates symlinks for any library matching::
93+
94+
lib<binary_name>bin.so
95+
96+
Each matching library is exposed at runtime as::
97+
98+
<binary_name>
99+
100+
For example::
101+
102+
libffmpegbin.so -> ffmpeg
103+
104+
Recipe developers may expose additional executables by renaming them
105+
using the same naming convention.
106+
107+
The following example performs a minimal FFmpeg sanity check using an
108+
in-memory test source and discards the output::
109+
110+
import subprocess
111+
112+
subprocess.run(
113+
["ffmpeg", "-f", "lavfi", "-i", "testsrc", "-t", "1", "-f", "null", "-"],
114+
check=True
115+
)
116+
117+
This verifies that ``ffmpeg`` is available and executable on the device.
118+
Ensure ``ffmpeg`` is included in your ``requirements``.
119+
120+
If video encoding is required, the following codec options must also be
121+
enabled in the build configuration:
122+
123+
- ``av_codecs``
124+
- ``libx264``
125+
126+
Without these, FFmpeg may be present but lack the required codec support.
127+
128+
See also: `APK native library execution restrictions <https://github.com/agnostic-apollo/Android-Docs/blob/master/site/pages/en/projects/docs/apps/processes/app-data-file-execute-restrictions.md>`_
129+
83130
Dismissing the splash screen
84131
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
85132

pythonforandroid/androidndk.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ def llvm_readelf(self):
6666
def llvm_strip(self):
6767
return f"{self.llvm_binutils_prefix}strip"
6868

69+
@property
70+
def llvm_nm(self):
71+
return f"{self.llvm_binutils_prefix}nm"
72+
6973
@property
7074
def sysroot(self):
7175
return os.path.join(self.llvm_prebuilt_dir, "sysroot")

pythonforandroid/bootstraps/common/build/jni/application/src/start.c

Lines changed: 142 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
#include <stdlib.h>
1010
#include <unistd.h>
1111
#include <dirent.h>
12+
#include <dlfcn.h>
13+
#include <libgen.h>
1214
#include <jni.h>
1315
#include <sys/stat.h>
1416
#include <sys/types.h>
@@ -30,8 +32,13 @@
3032

3133
#define ENTRYPOINT_MAXLEN 128
3234
#define LOG(n, x) __android_log_write(ANDROID_LOG_INFO, (n), (x))
33-
#define LOGP(x) LOG("python", (x))
3435
#define P4A_MIN_VER 11
36+
static void LOGP(const char *fmt, ...) {
37+
va_list args;
38+
va_start(args, fmt);
39+
__android_log_vprint(ANDROID_LOG_INFO, "python", fmt, args);
40+
va_end(args);
41+
}
3542

3643
static PyObject *androidembed_log(PyObject *self, PyObject *args) {
3744
char *logstr = NULL;
@@ -69,14 +76,125 @@ int dir_exists(char *filename) {
6976
}
7077

7178
int file_exists(const char *filename) {
72-
FILE *file;
73-
if ((file = fopen(filename, "r"))) {
74-
fclose(file);
75-
return 1;
76-
}
77-
return 0;
79+
return access(filename, F_OK) == 0;
7880
}
7981

82+
static void get_dirname(const char *path, char *dir, size_t size) {
83+
strncpy(dir, path, size - 1);
84+
dir[size - 1] = '\0';
85+
char *last_slash = strrchr(dir, '/');
86+
if (last_slash) *last_slash = '\0';
87+
else dir[0] = '\0';
88+
}
89+
90+
// strip "lib" prefix and "bin.so" suffix
91+
static void get_exe_name(const char *filename, char *out, size_t size) {
92+
size_t len = strlen(filename);
93+
if (len < 7) { // too short to be valid
94+
strncpy(out, filename, size - 1);
95+
out[size - 1] = '\0';
96+
return;
97+
}
98+
99+
const char *start = filename;
100+
if (strncmp(filename, "lib", 3) == 0) start += 3;
101+
size_t start_len = strlen(start);
102+
103+
if (start_len > 6) {
104+
size_t copy_len = start_len - 6; // remove "bin.so"
105+
if (copy_len >= size) copy_len = size - 1;
106+
strncpy(out, start, copy_len);
107+
out[copy_len] = '\0';
108+
} else {
109+
strncpy(out, start, size - 1);
110+
out[size - 1] = '\0';
111+
}
112+
}
113+
114+
char *setup_symlinks() {
115+
Dl_info info;
116+
char lib_path[512];
117+
char *interpreter = NULL;
118+
119+
if (!(dladdr((void*)setup_symlinks, &info) && info.dli_fname)) {
120+
LOGP("symlinking failed: failed to get libdir");
121+
return interpreter;
122+
}
123+
124+
strncpy(lib_path, info.dli_fname, sizeof(lib_path) - 1);
125+
lib_path[sizeof(lib_path) - 1] = '\0';
126+
127+
char native_lib_dir[512];
128+
get_dirname(lib_path, native_lib_dir, sizeof(native_lib_dir));
129+
if (native_lib_dir[0] == '\0') {
130+
LOGP("symlinking failed: could not determine lib directory");
131+
return interpreter;
132+
}
133+
134+
const char *files_dir_env = getenv("ANDROID_APP_PATH");
135+
char bin_dir[512];
136+
137+
snprintf(bin_dir, sizeof(bin_dir), "%s/.bin", files_dir_env);
138+
if (mkdir(bin_dir, 0755) != 0 && errno != EEXIST) {
139+
LOGP("Failed to create .bin directory");
140+
return interpreter;
141+
}
142+
143+
DIR *dir = opendir(native_lib_dir);
144+
if (!dir) {
145+
LOGP("Failed to open native lib dir");
146+
return interpreter;
147+
}
148+
149+
struct dirent *entry;
150+
while ((entry = readdir(dir)) != NULL) {
151+
const char *name = entry->d_name;
152+
size_t len = strlen(name);
153+
154+
if (len < 7) continue;
155+
if (strcmp(name + len - 6, "bin.so") != 0) continue; // only bin.so at end
156+
157+
// get cleaned executable name
158+
char exe_name[128];
159+
get_exe_name(name, exe_name, sizeof(exe_name));
160+
161+
char src[512], dst[512];
162+
snprintf(src, sizeof(src), "%s/%s", native_lib_dir, name);
163+
snprintf(dst, sizeof(dst), "%s/%s", bin_dir, exe_name);
164+
165+
// interpreter found?
166+
if (strcmp(exe_name, "python") == 0) {
167+
interpreter = strdup(dst);
168+
}
169+
170+
struct stat st;
171+
if (lstat(dst, &st) == 0) continue; // already exists
172+
if (symlink(src, dst) == 0) {
173+
LOGP("symlink: %s -> %s", name, exe_name);
174+
} else {
175+
LOGP("Symlink failed");
176+
}
177+
}
178+
179+
closedir(dir);
180+
181+
// append bin_dir to PATH
182+
const char *old_path = getenv("PATH");
183+
char new_path[1024];
184+
if (old_path && strlen(old_path) > 0) {
185+
snprintf(new_path, sizeof(new_path), "%s:%s", old_path, bin_dir);
186+
} else {
187+
snprintf(new_path, sizeof(new_path), "%s", bin_dir);
188+
}
189+
setenv("PATH", new_path, 1);
190+
191+
// set lib path
192+
setenv("LD_LIBRARY_PATH", native_lib_dir, 1);
193+
194+
return interpreter;
195+
}
196+
197+
80198
/* int main(int argc, char **argv) { */
81199
int main(int argc, char *argv[]) {
82200

@@ -147,10 +265,11 @@ int main(int argc, char *argv[]) {
147265
LOGP("Warning: no p4a_env_vars.txt found / failed to open!");
148266
}
149267

150-
LOGP("Changing directory to the one provided by ANDROID_ARGUMENT");
151-
LOGP(env_argument);
268+
LOGP("Changing directory to '%s'", env_argument);
152269
chdir(env_argument);
153270

271+
char *interpreter = setup_symlinks();
272+
154273
#if PY_MAJOR_VERSION < 3
155274
Py_NoSiteFlag=1;
156275
#endif
@@ -257,12 +376,20 @@ int main(int argc, char *argv[]) {
257376
"sys.path.append('%s/site-packages')",
258377
python_bundle_dir);
259378

260-
PyRun_SimpleString("import sys\n"
261-
"sys.argv = ['notaninterpreterreally']\n"
262-
"from os.path import realpath, join, dirname");
379+
PyRun_SimpleString("import sys, os\n"
380+
"from os.path import realpath, join, dirname");
381+
382+
char buf_exec[512];
383+
char buf_argv[512];
384+
snprintf(buf_exec, sizeof(buf_exec), "sys.executable = '%s'\n", interpreter);
385+
snprintf(buf_argv, sizeof(buf_argv), "sys.argv = ['%s']\n", interpreter);
386+
PyRun_SimpleString(buf_exec);
387+
PyRun_SimpleString(buf_argv);
388+
263389
PyRun_SimpleString(add_site_packages_dir);
264390
/* "sys.path.append(join(dirname(realpath(__file__)), 'site-packages'))") */
265391
PyRun_SimpleString("sys.path = ['.'] + sys.path");
392+
PyRun_SimpleString("os.environ['PYTHONPATH'] = ':'.join(sys.path)");
266393
}
267394

268395
PyRun_SimpleString(
@@ -280,23 +407,12 @@ int main(int argc, char *argv[]) {
280407
" androidembed.log(l.replace('\\x00', ''))\n"
281408
" self.__buffer = lines[-1]\n"
282409
"sys.stdout = sys.stderr = LogFile()\n"
283-
"print('Android path', sys.path)\n"
284-
"# import os\n"
285-
"# print('os.environ is', os.environ)\n"
286410
"print('Android kivy bootstrap done. __name__ is', __name__)");
287411

288412
#if PY_MAJOR_VERSION < 3
289413
PyRun_SimpleString("import site; print site.getsitepackages()\n");
290414
#endif
291415

292-
LOGP("AND: Ran string");
293-
294-
/* run it !
295-
*/
296-
LOGP("Run user program, change dir and execute entrypoint");
297-
298-
/* Get the entrypoint, search the .pyc then .py
299-
*/
300416
char *dot = strrchr(env_entrypoint, '.');
301417
char *ext = ".pyc";
302418
if (dot <= 0) {
@@ -345,7 +461,6 @@ int main(int argc, char *argv[]) {
345461
LOGP(entrypoint);
346462
return -1;
347463
}
348-
349464
/* run python !
350465
*/
351466
ret = PyRun_SimpleFile(fd, entrypoint);
@@ -361,34 +476,16 @@ int main(int argc, char *argv[]) {
361476

362477
LOGP("Python for android ended.");
363478

364-
/* Shut down: since regular shutdown causes issues sometimes
365-
(seems to be an incomplete shutdown breaking next launch)
366-
we'll use sys.exit(ret) to shutdown, since that one works.
367-
368-
Reference discussion:
369-
370-
https://github.com/kivy/kivy/pull/6107#issue-246120816
371-
*/
372-
char terminatecmd[256];
373-
snprintf(
374-
terminatecmd, sizeof(terminatecmd),
375-
"import sys; sys.exit(%d)\n", ret
376-
);
377-
PyRun_SimpleString(terminatecmd);
378-
379-
/* This should never actually be reached, but we'll leave the clean-up
380-
* here just to be safe.
381-
*/
382479
#if PY_MAJOR_VERSION < 3
383480
Py_Finalize();
384481
LOGP("Unexpectedly reached Py_FinalizeEx(), but was successful.");
385482
#else
386-
if (Py_FinalizeEx() != 0) // properly check success on Python 3
483+
if (Py_FinalizeEx() != 0) { // properly check success on Python 3
387484
LOGP("Unexpectedly reached Py_FinalizeEx(), and got error!");
388-
else
389-
LOGP("Unexpectedly reached Py_FinalizeEx(), but was successful.");
485+
}
390486
#endif
391487

488+
exit(ret);
392489
return ret;
393490
}
394491

0 commit comments

Comments
 (0)