Skip to content

Commit a76441a

Browse files
committed
boostrap: add an option in the build.py to use assets/ directory for packaging sources instead of tar->.mp3. Python now use directly the APK to load its own sources, except Makefile and pyconfig.h. Only the main.pyo is extracted and executed.
1 parent b50a430 commit a76441a

File tree

2 files changed

+145
-81
lines changed

2 files changed

+145
-81
lines changed

bootstrap/common/build.py

Lines changed: 58 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env python2.7
22

3-
from os.path import dirname, join, isfile, realpath, relpath, split
3+
from os.path import dirname, join, isfile, realpath, relpath, split, exists
44
from zipfile import ZipFile
55
import sys
66
sys.path.insert(0, 'buildlib/jinja2.egg')
@@ -120,7 +120,7 @@ def listfiles(d):
120120
yield fn
121121

122122

123-
def make_pythonzip():
123+
def make_pythonzip(use_assets):
124124
'''
125125
Search for all the python related files, and construct the pythonXX.zip
126126
According to
@@ -147,22 +147,27 @@ def select(fn):
147147
# get a list of all python file
148148
python_files = [x for x in listfiles(d) if select(x)]
149149

150-
# create the final zipfile
151-
zfn = join('private', 'lib', 'python27.zip')
152-
zf = ZipFile(zfn, 'w')
153-
154-
# put all the python files in it
155-
for fn in python_files:
156-
afn = fn[len(d):]
157-
zf.write(fn, afn)
158-
zf.close()
150+
if use_assets:
151+
for fn in python_files:
152+
afn = fn[len(d) + 1:]
153+
dest_fn = join("assets", "lib", "python2.7", afn)
154+
dest_dir = dirname(dest_fn)
155+
if not exists(dest_dir):
156+
os.makedirs(dest_dir)
157+
shutil.copy(fn, dest_fn)
158+
else:
159+
# create the final zipfile
160+
zfn = join('private', 'lib', 'python27.zip')
161+
zf = ZipFile(zfn, 'w')
159162

163+
# put all the python files in it
164+
for fn in python_files:
165+
afn = fn[len(d):]
166+
zf.write(fn, afn)
167+
zf.close()
160168

161-
def make_tar(tfn, source_dirs, ignore_path=[]):
162-
'''
163-
Make a zip file `fn` from the contents of source_dis.
164-
'''
165169

170+
def iterate_sources(source_dirs, ignore_path=[]):
166171
# selector function
167172
def select(fn):
168173
rfn = realpath(fn)
@@ -176,17 +181,24 @@ def select(fn):
176181
return not is_blacklist(fn)
177182

178183
# get the files and relpath file of all the directory we asked for
179-
files = []
180184
for sd in source_dirs:
181185
sd = realpath(sd)
182186
compile_dir(sd)
183-
files += [(x, relpath(realpath(x), sd)) for x in listfiles(sd)
184-
if select(x)]
187+
for x in listfiles(sd):
188+
if not select(x):
189+
continue
190+
yield (x, relpath(realpath(x), sd))
191+
192+
193+
def make_tar(tfn, source_dirs, ignore_path=[]):
194+
'''
195+
Make a zip file `fn` from the contents of source_dis.
196+
'''
185197

186198
# create tar.gz of thoses files
187199
tf = tarfile.open(tfn, 'w:gz')
188200
dirs = []
189-
for fn, afn in files:
201+
for fn, afn in iterate_sources(source_dirs, ignore_path):
190202
print '%s: %s' % (tfn, fn)
191203
dn = dirname(afn)
192204
if dn not in dirs:
@@ -208,6 +220,17 @@ def select(fn):
208220
tf.close()
209221

210222

223+
def copy_to_assets(source_dirs, ignore_path=[]):
224+
print 'copy_to_assets()', source_dirs, ignore_path
225+
for fn, afn in iterate_sources(source_dirs, ignore_path):
226+
print '{}: {}'.format(fn, afn)
227+
dest_fn = join('assets', afn)
228+
dest_dir = dirname(dest_fn)
229+
if not exists(dest_dir):
230+
os.makedirs(dest_dir)
231+
shutil.copy(fn, dest_fn)
232+
233+
211234
def make_package(args):
212235
version_code = 0
213236
manifest_extra = '<uses-feature android:glEsVersion="0x00020000" />'
@@ -321,16 +344,23 @@ def make_package(args):
321344

322345
# In order to speedup import and initial depack,
323346
# construct a python27.zip
324-
make_pythonzip()
347+
make_pythonzip(args.use_assets)
325348

326349
# Package up the private and public data.
327-
if args.private:
328-
make_tar('assets/private.mp3', ['private', args.private])
350+
if args.use_assets:
351+
if args.private:
352+
copy_to_assets(['private', args.private])
353+
else:
354+
copy_to_assets(['private'])
355+
if args.dir:
356+
copy_to_assets([args.dir], args.ignore_path)
329357
else:
330-
make_tar('assets/private.mp3', ['private'])
331-
332-
if args.dir:
333-
make_tar('assets/public.mp3', [args.dir], args.ignore_path)
358+
if args.private:
359+
make_tar('assets/private.mp3', ['private', args.private])
360+
else:
361+
make_tar('assets/private.mp3', ['private'])
362+
if args.dir:
363+
make_tar('assets/public.mp3', [args.dir], args.ignore_path)
334364

335365
# Copy over the icon and presplash files.
336366
shutil.copy(args.icon or default_icon, 'res/drawable/icon.png')
@@ -370,7 +400,8 @@ def make_package(args):
370400
For this to work, Java and Ant need to be in your path, as does the
371401
tools directory of the Android SDK.
372402
''')
373-
403+
ap.add_argument('--assets', dest='use_assets', action='store_true',
404+
help=('Copy the app into assets/ instead of .tar'))
374405
ap.add_argument('--package', dest='package',
375406
help=('The name of the java package the project will be'
376407
' packaged under.'),

bootstrap/minimal/jni/main.c

Lines changed: 87 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -31,23 +31,34 @@ PyMODINIT_FUNC initandroidembed(void) {
3131
(void) Py_InitModule("androidembed", AndroidEmbedMethods);
3232
}
3333

34-
int file_exists(const char * filename)
35-
{
36-
FILE *file;
37-
if (file = fopen(filename, "r")) {
38-
fclose(file);
39-
return 1;
34+
int asset_extract(AAssetManager *am, char *src_file, char *dst_file) {
35+
FILE *fhd = fopen(dst_file, "wb");
36+
if (fhd == NULL) {
37+
LOGW("Unable to open descriptor for %s (errno=%d:%s)",
38+
dst_file, errno, strerror(errno));
39+
return -1;
4040
}
41+
42+
AAsset *asset = AAssetManager_open(am, src_file, AASSET_MODE_BUFFER);
43+
if (asset == NULL) {
44+
LOGW("Unable to open asset %s", src_file);
45+
return -1;
46+
}
47+
48+
off_t l = AAsset_getLength(asset);
49+
fwrite(AAsset_getBuffer(asset),l, 1, fhd);
50+
fclose(fhd);
51+
AAsset_close(asset);
52+
4153
return 0;
4254
}
4355

44-
4556
void android_main(struct android_app* state) {
4657
app_dummy();
4758
LOGI("Starting minimal bootstrap.");
4859

4960
char *env_argument = NULL;
50-
FILE *fd;
61+
int fd = -1;
5162

5263
LOGI("Initialize Python for Android");
5364
//env_argument = getenv("ANDROID_ARGUMENT");
@@ -57,30 +68,33 @@ void android_main(struct android_app* state) {
5768
Py_Initialize();
5869
//PySys_SetArgv(argc, argv);
5970

60-
/* ensure threads will work.
61-
*/
71+
// ensure threads will work.
6272
PyEval_InitThreads();
6373

64-
/* our logging module for android
65-
*/
74+
// our logging module for android
6675
initandroidembed();
6776

68-
/* inject our bootstrap code to redirect python stdin/stdout
69-
* replace sys.path with our path
70-
*/
71-
#if 0
72-
PyRun_SimpleString(
73-
"import sys, posix\n" \
74-
"private = posix.environ['ANDROID_PRIVATE']\n" \
75-
"argument = posix.environ['ANDROID_ARGUMENT']\n" \
76-
"sys.path[:] = [ \n" \
77-
" private + '/lib/python27.zip', \n" \
78-
" private + '/lib/python2.7/', \n" \
79-
" private + '/lib/python2.7/lib-dynload/', \n" \
80-
" private + '/lib/python2.7/site-packages/', \n" \
81-
" argument ]\n")
82-
#endif
83-
77+
// get the APK filename, and set it to ANDROID_APK_FN
78+
ANativeActivity* activity = state->activity;
79+
JNIEnv* env = NULL;
80+
(*activity->vm)->AttachCurrentThread(activity->vm, &env, 0);
81+
jclass clazz = (*env)->GetObjectClass(env, activity->clazz);
82+
jmethodID methodID = (*env)->GetMethodID(env, clazz, "getPackageCodePath", "()Ljava/lang/String;");
83+
jobject result = (*env)->CallObjectMethod(env, activity->clazz, methodID);
84+
const char* str;
85+
jboolean isCopy;
86+
str = (*env)->GetStringUTFChars(env, (jstring)result, &isCopy);
87+
LOGI("Looked up package code path: %s", str);
88+
(*activity->vm)->DetachCurrentThread(activity->vm);
89+
90+
// set some envs
91+
setenv("ANDROID_APK_FN", str, 1);
92+
setenv("ANDROID_INTERNAL_DATA_PATH", state->activity->internalDataPath, 1);
93+
setenv("ANDROID_EXTERNAL_DATA_PATH", state->activity->externalDataPath, 1);
94+
LOGI("Internal data path is: %s", state->activity->internalDataPath);
95+
LOGI("External data path is: %s", state->activity->externalDataPath);
96+
97+
// inject our bootstrap code to redirect python stdin/stdout
8498
PyRun_SimpleString(
8599
"import sys, androidembed\n" \
86100
"class LogFile(object):\n" \
@@ -94,42 +108,62 @@ void android_main(struct android_app* state) {
94108
" self.buffer = lines[-1]\n" \
95109
" def flush(self):\n" \
96110
" return\n" \
97-
"sys.stdout = sys.stderr = LogFile()\n" \
111+
"sys.stdout = sys.stderr = LogFile()\n");
112+
113+
// let python knows where the python2.7 library is within the APK
114+
PyRun_SimpleString(
115+
"import sys, posix;" \
116+
"lib_path = '{}/assets/lib/python2.7/'.format(" \
117+
" posix.environ['ANDROID_APK_FN'])\n" \
118+
"sys.path[:] = [lib_path, '{}/site-packages'.format(lib_path)]\n" \
119+
"import os; from os.path import exists, join\n" \
120+
"config_path = join(posix.environ['ANDROID_INTERNAL_DATA_PATH'], 'python2.7', 'config')\n" \
121+
"if not exists(config_path): os.makedirs(config_path)\n" \
122+
"import sysconfig\n" \
123+
"sysconfig._get_makefile_filename = lambda: '{}/Makefile'.format(config_path)\n" \
124+
"sysconfig.get_config_h_filename = lambda: '{}/pyconfig.h'.format(config_path)\n" \
125+
);
126+
127+
// extract the Makefile, needed for sysconfig
128+
AAssetManager *am = state->activity->assetManager;
129+
char dest_fn[512];
130+
131+
snprintf(dest_fn, 512, "%s/python2.7/config/Makefile", state->activity->internalDataPath);
132+
if (asset_extract(am, "lib/python2.7/config/Makefile", dest_fn) < 0)
133+
return;
134+
135+
snprintf(dest_fn, 512, "%s/python2.7/config/pyconfig.h", state->activity->internalDataPath);
136+
if (asset_extract(am, "include/python2.7/pyconfig.h", dest_fn) < 0)
137+
return;
138+
139+
// test import site
140+
PyRun_SimpleString(
98141
"import site; print site.getsitepackages()\n"\
99142
"print 'Android path', sys.path\n" \
100-
"print 'Android kivy bootstrap done. __name__ is', __name__");
143+
"print 'Android bootstrap done. __name__ is', __name__");
101144

102145
/* run it !
103146
*/
104-
LOGI("Run user program, change dir and execute main.py");
105-
//chdir(env_argument);
106-
107-
/* search the initial main.py
108-
*/
109-
char *main_py = "main.pyo";
110-
if ( file_exists(main_py) == 0 ) {
111-
if ( file_exists("main.py") )
112-
main_py = "main.py";
113-
else
114-
main_py = NULL;
115-
}
116-
117-
if ( main_py == NULL ) {
118-
LOGW("No main.pyo / main.py found.");
119-
return;
120-
}
121-
122-
fd = fopen(main_py, "r");
123-
if ( fd == NULL ) {
124-
LOGW("Open the main.py(o) failed");
147+
LOGI("Extract main.py from assets");
148+
char main_fn[512];
149+
snprintf(main_fn, 512, "%s/main.pyo", state->activity->internalDataPath);
150+
if (asset_extract(am, "main.pyo", main_fn) < 0)
125151
return;
126-
}
127152

128153
/* run python !
129154
*/
130-
PyRun_SimpleFile(fd, main_py);
155+
LOGI("Run main.py >>>");
156+
FILE *fhd = fopen(main_fn, "rb");
157+
if (fhd == NULL) {
158+
LOGW("Cannot open main.pyo (errno=%d:%s)", errno, strerror(errno));
159+
return;
160+
}
161+
int ret = PyRun_SimpleFile(fhd, main_fn);
162+
fclose(fhd);
163+
LOGI("Run main.py (ret=%d) <<<", ret);
131164

132165
if (PyErr_Occurred() != NULL) {
166+
LOGW("An error occured.");
133167
PyErr_Print(); /* This exits with the right code if SystemExit. */
134168
if (Py_FlushLine())
135169
PyErr_Clear();
@@ -138,7 +172,6 @@ void android_main(struct android_app* state) {
138172
/* close everything
139173
*/
140174
Py_Finalize();
141-
fclose(fd);
142175

143176
LOGW("Python for android ended.");
144177
}

0 commit comments

Comments
 (0)