Merge branch 'master' of https://github.com/kivy/buildozer
This commit is contained in:
commit
8e96078ad1
6 changed files with 586 additions and 24 deletions
|
@ -12,14 +12,15 @@ Layout directory for buildozer:
|
||||||
|
|
||||||
__version__ = '0.3-dev'
|
__version__ = '0.3-dev'
|
||||||
|
|
||||||
import shelve
|
|
||||||
import zipfile
|
|
||||||
import sys
|
|
||||||
import fcntl
|
import fcntl
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import shelve
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import zipfile
|
||||||
from select import select
|
from select import select
|
||||||
from sys import stdout, stderr, exit
|
from sys import stdout, stderr, stdin, exit
|
||||||
from urllib import urlretrieve
|
from urllib import urlretrieve
|
||||||
from re import search
|
from re import search
|
||||||
from ConfigParser import SafeConfigParser
|
from ConfigParser import SafeConfigParser
|
||||||
|
@ -29,6 +30,14 @@ from os import environ, unlink, rename, walk, sep, listdir, makedirs
|
||||||
from copy import copy
|
from copy import copy
|
||||||
from shutil import copyfile, rmtree, copytree
|
from shutil import copyfile, rmtree, copytree
|
||||||
|
|
||||||
|
# windows does not have termios...
|
||||||
|
try:
|
||||||
|
import termios
|
||||||
|
import tty
|
||||||
|
has_termios = True
|
||||||
|
except ImportError:
|
||||||
|
has_termios = False
|
||||||
|
|
||||||
RESET_SEQ = "\033[0m"
|
RESET_SEQ = "\033[0m"
|
||||||
COLOR_SEQ = "\033[1;{0}m"
|
COLOR_SEQ = "\033[1;{0}m"
|
||||||
BOLD_SEQ = "\033[1m"
|
BOLD_SEQ = "\033[1m"
|
||||||
|
@ -53,6 +62,7 @@ class Buildozer(object):
|
||||||
self.environ = {}
|
self.environ = {}
|
||||||
self.specfilename = filename
|
self.specfilename = filename
|
||||||
self.state = None
|
self.state = None
|
||||||
|
self.build_id = None
|
||||||
self.config = SafeConfigParser()
|
self.config = SafeConfigParser()
|
||||||
self.config.getlist = self._get_config_list
|
self.config.getlist = self._get_config_list
|
||||||
self.config.getdefault = self._get_config_default
|
self.config.getdefault = self._get_config_default
|
||||||
|
@ -122,7 +132,15 @@ class Buildozer(object):
|
||||||
if hasattr(self.target, '_build_done'):
|
if hasattr(self.target, '_build_done'):
|
||||||
return
|
return
|
||||||
|
|
||||||
self.info('Build the application')
|
# increment the build number
|
||||||
|
self.build_id = int(self.state.get('cache.build_id', '0')) + 1
|
||||||
|
self.state['cache.build_id'] = str(self.build_id)
|
||||||
|
# FIXME WHY the hell we need to close/reopen the state to sync the build
|
||||||
|
# id ???
|
||||||
|
self.state.close()
|
||||||
|
self.state = shelve.open(join(self.buildozer_dir, 'state.db'))
|
||||||
|
|
||||||
|
self.info('Build the application #{}'.format(self.build_id))
|
||||||
self.build_application()
|
self.build_application()
|
||||||
|
|
||||||
self.info('Package the application')
|
self.info('Package the application')
|
||||||
|
@ -187,8 +205,16 @@ class Buildozer(object):
|
||||||
show_output = kwargs.pop('show_output')
|
show_output = kwargs.pop('show_output')
|
||||||
get_stdout = kwargs.pop('get_stdout', False)
|
get_stdout = kwargs.pop('get_stdout', False)
|
||||||
get_stderr = kwargs.pop('get_stderr', False)
|
get_stderr = kwargs.pop('get_stderr', False)
|
||||||
|
break_on_error = kwargs.pop('break_on_error', True)
|
||||||
|
sensible = kwargs.pop('sensible', False)
|
||||||
|
|
||||||
self.debug('Run {0!r}'.format(command))
|
if not sensible:
|
||||||
|
self.debug('Run {0!r}'.format(command))
|
||||||
|
else:
|
||||||
|
if type(command) in (list, tuple):
|
||||||
|
self.debug('Run {0!r} ...'.format(command[0]))
|
||||||
|
else:
|
||||||
|
self.debug('Run {0!r} ...'.format(command.split()[0]))
|
||||||
self.debug('Cwd {}'.format(kwargs.get('cwd')))
|
self.debug('Cwd {}'.format(kwargs.get('cwd')))
|
||||||
|
|
||||||
# open the process
|
# open the process
|
||||||
|
@ -229,14 +255,14 @@ class Buildozer(object):
|
||||||
stderr.flush()
|
stderr.flush()
|
||||||
|
|
||||||
process.communicate()
|
process.communicate()
|
||||||
if process.returncode != 0:
|
if process.returncode != 0 and break_on_error:
|
||||||
self.error('Command failed: {0}'.format(command))
|
self.error('Command failed: {0}'.format(command))
|
||||||
raise BuildozerCommandException()
|
raise BuildozerCommandException()
|
||||||
if ret_stdout:
|
if ret_stdout:
|
||||||
ret_stdout = ''.join(ret_stdout)
|
ret_stdout = ''.join(ret_stdout)
|
||||||
if ret_stderr:
|
if ret_stderr:
|
||||||
ret_stderr = ''.join(ret_stderr)
|
ret_stderr = ''.join(ret_stderr)
|
||||||
return (ret_stdout, ret_stderr)
|
return (ret_stdout, ret_stderr, process.returncode)
|
||||||
|
|
||||||
def check_configuration_tokens(self):
|
def check_configuration_tokens(self):
|
||||||
'''Ensure the spec file is 'correct'.
|
'''Ensure the spec file is 'correct'.
|
||||||
|
@ -384,6 +410,13 @@ class Buildozer(object):
|
||||||
self.debug('Rename {0} to {1}'.format(source, target))
|
self.debug('Rename {0} to {1}'.format(source, target))
|
||||||
rename(source, target)
|
rename(source, target)
|
||||||
|
|
||||||
|
def file_copy(self, source, target, cwd=None):
|
||||||
|
if cwd:
|
||||||
|
source = join(cwd, source)
|
||||||
|
target = join(cwd, target)
|
||||||
|
self.debug('Copy {0} to {1}'.format(source, target))
|
||||||
|
copyfile(source, target)
|
||||||
|
|
||||||
def file_extract(self, archive, cwd=None):
|
def file_extract(self, archive, cwd=None):
|
||||||
if archive.endswith('.tgz') or archive.endswith('.tar.gz'):
|
if archive.endswith('.tgz') or archive.endswith('.tar.gz'):
|
||||||
# XXX tarfile doesn't work for NDK-r8c :(
|
# XXX tarfile doesn't work for NDK-r8c :(
|
||||||
|
@ -467,11 +500,18 @@ class Buildozer(object):
|
||||||
raise Exception('Missing version or version.regex + version.filename')
|
raise Exception('Missing version or version.regex + version.filename')
|
||||||
|
|
||||||
def build_application(self):
|
def build_application(self):
|
||||||
|
self._copy_application_sources()
|
||||||
|
self._copy_application_libs()
|
||||||
|
self._patch_application_sources()
|
||||||
|
|
||||||
|
def _copy_application_sources(self):
|
||||||
source_dir = realpath(self.config.getdefault('app', 'source.dir', '.'))
|
source_dir = realpath(self.config.getdefault('app', 'source.dir', '.'))
|
||||||
include_exts = self.config.getlist('app', 'source.include_exts', '')
|
include_exts = self.config.getlist('app', 'source.include_exts', '')
|
||||||
exclude_exts = self.config.getlist('app', 'source.exclude_exts', '')
|
exclude_exts = self.config.getlist('app', 'source.exclude_exts', '')
|
||||||
app_dir = self.app_dir
|
app_dir = self.app_dir
|
||||||
|
|
||||||
|
self.debug('Copy application source from {}'.format(source_dir))
|
||||||
|
|
||||||
rmtree(self.app_dir)
|
rmtree(self.app_dir)
|
||||||
|
|
||||||
for root, dirs, files in walk(source_dir):
|
for root, dirs, files in walk(source_dir):
|
||||||
|
@ -505,11 +545,13 @@ class Buildozer(object):
|
||||||
self.debug('Copy {0}'.format(sfn))
|
self.debug('Copy {0}'.format(sfn))
|
||||||
copyfile(sfn, rfn)
|
copyfile(sfn, rfn)
|
||||||
|
|
||||||
|
def _copy_application_libs(self):
|
||||||
# copy also the libs
|
# copy also the libs
|
||||||
copytree(self.applibs_dir, join(app_dir, '_applibs'))
|
copytree(self.applibs_dir, join(self.app_dir, '_applibs'))
|
||||||
|
|
||||||
|
def _patch_application_sources(self):
|
||||||
# patch the main.py
|
# patch the main.py
|
||||||
main_py = join(app_dir, 'main.py')
|
main_py = join(self.app_dir, 'main.py')
|
||||||
if not self.file_exists(main_py):
|
if not self.file_exists(main_py):
|
||||||
self.error('Unable to patch main_py to add applibs directory.')
|
self.error('Unable to patch main_py to add applibs directory.')
|
||||||
return
|
return
|
||||||
|
@ -564,6 +606,13 @@ class Buildozer(object):
|
||||||
def global_cache_dir(self):
|
def global_cache_dir(self):
|
||||||
return join(self.global_buildozer_dir, 'cache')
|
return join(self.global_buildozer_dir, 'cache')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def package_full_name(self):
|
||||||
|
package_name = self.config.getdefault('app', 'package.name', '')
|
||||||
|
package_domain = self.config.getdefault('app', 'package.domain', '')
|
||||||
|
if package_domain == '':
|
||||||
|
return package_name
|
||||||
|
return '{}.{}'.format(package_domain, package_name)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -582,6 +631,7 @@ class Buildozer(object):
|
||||||
fromlist=['buildozer'])
|
fromlist=['buildozer'])
|
||||||
yield target, m
|
yield target, m
|
||||||
except:
|
except:
|
||||||
|
raise
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def usage(self):
|
def usage(self):
|
||||||
|
@ -650,7 +700,7 @@ class Buildozer(object):
|
||||||
self.usage()
|
self.usage()
|
||||||
exit(0)
|
exit(0)
|
||||||
|
|
||||||
if args == '--version':
|
if arg == '--version':
|
||||||
print 'Buildozer {0}'.format(__version__)
|
print 'Buildozer {0}'.format(__version__)
|
||||||
exit(0)
|
exit(0)
|
||||||
|
|
||||||
|
@ -725,6 +775,224 @@ class Buildozer(object):
|
||||||
return self.config.get(section, token)
|
return self.config.get(section, token)
|
||||||
|
|
||||||
|
|
||||||
|
class BuildozerRemote(Buildozer):
|
||||||
|
def run_command(self, args):
|
||||||
|
while args:
|
||||||
|
if not args[0].startswith('-'):
|
||||||
|
break
|
||||||
|
arg = args.pop(0)
|
||||||
|
|
||||||
|
if arg in ('-v', '--verbose'):
|
||||||
|
self.log_level = 2
|
||||||
|
|
||||||
|
if arg in ('-h', '--help'):
|
||||||
|
self.usage()
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
if arg == '--version':
|
||||||
|
print 'Buildozer (remote) {0}'.format(__version__)
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
if len(args) < 2:
|
||||||
|
self.usage()
|
||||||
|
return
|
||||||
|
|
||||||
|
remote_name = args[0]
|
||||||
|
remote_section = 'remote:{}'.format(remote_name)
|
||||||
|
if not self.config.has_section(remote_section):
|
||||||
|
self.error('Unknow remote "{}", must be configured first.'.format(
|
||||||
|
remote_name))
|
||||||
|
return
|
||||||
|
|
||||||
|
self.remote_host = remote_host = self.config.get(
|
||||||
|
remote_section, 'host', '')
|
||||||
|
self.remote_user = remote_user = self.config.get(
|
||||||
|
remote_section, 'user', '')
|
||||||
|
self.remote_build_dir = remote_build_dir = self.config.get(
|
||||||
|
remote_section, 'build_directory', '')
|
||||||
|
if not remote_host:
|
||||||
|
self.error('Missing "host = " for {}'.format(remote_section))
|
||||||
|
return
|
||||||
|
if not remote_user:
|
||||||
|
self.error('Missing "user = " for {}'.format(remote_section))
|
||||||
|
return
|
||||||
|
if not remote_build_dir:
|
||||||
|
self.error('Missing "build_directory = " for {}'.format(remote_section))
|
||||||
|
return
|
||||||
|
|
||||||
|
# fake the target
|
||||||
|
self.targetname = 'remote'
|
||||||
|
self.check_build_layout()
|
||||||
|
|
||||||
|
# prepare our source code
|
||||||
|
self.info('Prepare source code to sync')
|
||||||
|
self._copy_application_sources()
|
||||||
|
self._ssh_connect()
|
||||||
|
try:
|
||||||
|
self._ensure_buildozer()
|
||||||
|
self._sync_application_sources()
|
||||||
|
self._do_remote_commands(args[1:])
|
||||||
|
finally:
|
||||||
|
self._ssh_close()
|
||||||
|
|
||||||
|
def _ssh_connect(self):
|
||||||
|
self.info('Connecting to {}'.format(self.remote_host))
|
||||||
|
import paramiko
|
||||||
|
self._ssh_client = client = paramiko.SSHClient()
|
||||||
|
client.load_system_host_keys()
|
||||||
|
client.connect(self.remote_host, username=self.remote_user)
|
||||||
|
self._sftp_client = client.open_sftp()
|
||||||
|
|
||||||
|
def _ssh_close(self):
|
||||||
|
self.debug('Closing remote connection')
|
||||||
|
self._sftp_client.close()
|
||||||
|
self._ssh_client.close()
|
||||||
|
|
||||||
|
def _ensure_buildozer(self):
|
||||||
|
s = self._sftp_client
|
||||||
|
root_dir = s.normalize('.')
|
||||||
|
self.remote_build_dir = join(root_dir, self.remote_build_dir,
|
||||||
|
self.package_full_name)
|
||||||
|
self.debug('Remote build directory: {}'.format(self.remote_build_dir))
|
||||||
|
self._ssh_mkdir(self.remote_build_dir)
|
||||||
|
self._ssh_sync(__path__[0])
|
||||||
|
|
||||||
|
def _sync_application_sources(self):
|
||||||
|
self.info('Synchronize application sources')
|
||||||
|
self._ssh_sync(self.app_dir)
|
||||||
|
|
||||||
|
# create custom buildozer.spec
|
||||||
|
self.info('Create custom buildozer.spec')
|
||||||
|
config = SafeConfigParser()
|
||||||
|
config.read('buildozer.spec')
|
||||||
|
config.set('app', 'source.dir', 'app')
|
||||||
|
|
||||||
|
fn = join(self.remote_build_dir, 'buildozer.spec')
|
||||||
|
fd = self._sftp_client.open(fn, 'wb')
|
||||||
|
config.write(fd)
|
||||||
|
fd.close()
|
||||||
|
|
||||||
|
def _do_remote_commands(self, args):
|
||||||
|
self.info('Execute remote buildozer')
|
||||||
|
cmd = (
|
||||||
|
'source ~/.profile;'
|
||||||
|
'cd {0};'
|
||||||
|
'env PYTHONPATH={0}:$PYTHONPATH '
|
||||||
|
'python -c "import buildozer, sys;'
|
||||||
|
'buildozer.Buildozer().run_command(sys.argv[1:])" {1} {2} 2>&1').format(
|
||||||
|
self.remote_build_dir,
|
||||||
|
'--verbose' if self.log_level == 2 else '',
|
||||||
|
' '.join(args),
|
||||||
|
)
|
||||||
|
self._ssh_command(cmd)
|
||||||
|
|
||||||
|
def _ssh_mkdir(self, *args):
|
||||||
|
directory = join(*args)
|
||||||
|
self.debug('Create remote directory {}'.format(directory))
|
||||||
|
try:
|
||||||
|
self._sftp_client.mkdir(directory)
|
||||||
|
except IOError:
|
||||||
|
# already created?
|
||||||
|
try:
|
||||||
|
self._sftp_client.stat(directory)
|
||||||
|
except IOError:
|
||||||
|
self.error('Unable to create remote directory {}'.format(directory))
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _ssh_sync(self, directory):
|
||||||
|
self.debug('Syncing {} directory'.format(directory))
|
||||||
|
directory = realpath(directory)
|
||||||
|
base_strip = directory.rfind('/')
|
||||||
|
for root, dirs, files in walk(directory):
|
||||||
|
self._ssh_mkdir(self.remote_build_dir, root[base_strip + 1:])
|
||||||
|
for fn in files:
|
||||||
|
if splitext(fn)[1] in ('.pyo', '.pyc', '.swp'):
|
||||||
|
continue
|
||||||
|
local_file = join(root, fn)
|
||||||
|
remote_file = join(self.remote_build_dir, root[base_strip + 1:], fn)
|
||||||
|
self.debug('Sync {} -> {}'.format(local_file, remote_file))
|
||||||
|
self._sftp_client.put(local_file, remote_file)
|
||||||
|
|
||||||
|
def _ssh_command(self, command):
|
||||||
|
self.debug('Execute remote command {}'.format(command))
|
||||||
|
#shell = self._ssh_client.invoke_shell()
|
||||||
|
#shell.sendall(command)
|
||||||
|
#shell.sendall('\nexit\n')
|
||||||
|
transport = self._ssh_client.get_transport()
|
||||||
|
channel = transport.open_session()
|
||||||
|
try:
|
||||||
|
channel.exec_command(command)
|
||||||
|
self._interactive_shell(channel)
|
||||||
|
finally:
|
||||||
|
channel.close()
|
||||||
|
|
||||||
|
def usage(self):
|
||||||
|
print 'Usage: buildozer-remote [--verbose] [remote-name] [buildozer args]'
|
||||||
|
|
||||||
|
|
||||||
|
def _interactive_shell(self, chan):
|
||||||
|
if has_termios:
|
||||||
|
self._posix_shell(chan)
|
||||||
|
else:
|
||||||
|
self._windows_shell(chan)
|
||||||
|
|
||||||
|
def _posix_shell(self, chan):
|
||||||
|
oldtty = termios.tcgetattr(stdin)
|
||||||
|
try:
|
||||||
|
#tty.setraw(stdin.fileno())
|
||||||
|
#tty.setcbreak(stdin.fileno())
|
||||||
|
chan.settimeout(0.0)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
r, w, e = select([chan, stdin], [], [])
|
||||||
|
if chan in r:
|
||||||
|
try:
|
||||||
|
x = chan.recv(128)
|
||||||
|
if len(x) == 0:
|
||||||
|
print '\r\n*** EOF\r\n',
|
||||||
|
break
|
||||||
|
stdout.write(x)
|
||||||
|
stdout.flush()
|
||||||
|
#print len(x), repr(x)
|
||||||
|
except socket.timeout:
|
||||||
|
pass
|
||||||
|
if stdin in r:
|
||||||
|
x = stdin.read(1)
|
||||||
|
if len(x) == 0:
|
||||||
|
break
|
||||||
|
chan.sendall(x)
|
||||||
|
finally:
|
||||||
|
termios.tcsetattr(stdin, termios.TCSADRAIN, oldtty)
|
||||||
|
|
||||||
|
# thanks to Mike Looijmans for this code
|
||||||
|
def _windows_shell(self,chan):
|
||||||
|
import threading
|
||||||
|
|
||||||
|
stdout.write("Line-buffered terminal emulation. Press F6 or ^Z to send EOF.\r\n\r\n")
|
||||||
|
|
||||||
|
def writeall(sock):
|
||||||
|
while True:
|
||||||
|
data = sock.recv(256)
|
||||||
|
if not data:
|
||||||
|
stdout.write('\r\n*** EOF ***\r\n\r\n')
|
||||||
|
stdout.flush()
|
||||||
|
break
|
||||||
|
stdout.write(data)
|
||||||
|
stdout.flush()
|
||||||
|
|
||||||
|
writer = threading.Thread(target=writeall, args=(chan,))
|
||||||
|
writer.start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
d = stdin.read(1)
|
||||||
|
if not d:
|
||||||
|
break
|
||||||
|
chan.send(d)
|
||||||
|
except EOFError:
|
||||||
|
# user hit ^Z or F6
|
||||||
|
pass
|
||||||
|
|
||||||
def run():
|
def run():
|
||||||
try:
|
try:
|
||||||
Buildozer().run_command(sys.argv[1:])
|
Buildozer().run_command(sys.argv[1:])
|
||||||
|
@ -733,3 +1001,8 @@ def run():
|
||||||
# command failed.
|
# command failed.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def run_remote():
|
||||||
|
try:
|
||||||
|
BuildozerRemote().run_command(sys.argv[1:])
|
||||||
|
except BuildozerCommandException:
|
||||||
|
pass
|
||||||
|
|
|
@ -13,7 +13,7 @@ package.domain = org.test
|
||||||
source.dir = .
|
source.dir = .
|
||||||
|
|
||||||
# (list) Source files to include (let empty to include all the files)
|
# (list) Source files to include (let empty to include all the files)
|
||||||
source.include_exts = py,png,jpg
|
source.include_exts = py,png,jpg,kv,atlas
|
||||||
|
|
||||||
# (list) Source files to exclude (let empty to not excluding anything)
|
# (list) Source files to exclude (let empty to not excluding anything)
|
||||||
#source.exclude_exts = spec
|
#source.exclude_exts = spec
|
||||||
|
@ -28,6 +28,12 @@ version.filename = %(source.dir)s/main.py
|
||||||
# (list) Application requirements
|
# (list) Application requirements
|
||||||
requirements = twisted,kivy
|
requirements = twisted,kivy
|
||||||
|
|
||||||
|
# (str) Presplash of the application
|
||||||
|
#presplash.filename = %(source.dir)s/data/presplash.png
|
||||||
|
|
||||||
|
# (str) Icon of the application
|
||||||
|
#icon.filename = %(source.dir)s/data/icon.png
|
||||||
|
|
||||||
#
|
#
|
||||||
# Android specific
|
# Android specific
|
||||||
#
|
#
|
||||||
|
@ -56,6 +62,18 @@ requirements = twisted,kivy
|
||||||
# (str) Android entry point, default is ok for Kivy-based app
|
# (str) Android entry point, default is ok for Kivy-based app
|
||||||
#android.entrypoint = org.renpy.android.PythonActivity
|
#android.entrypoint = org.renpy.android.PythonActivity
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# iOS specific
|
||||||
|
#
|
||||||
|
|
||||||
|
# (str) Name of the certificate to use for signing the debug version
|
||||||
|
#ios.codesign.debug = "iPhone Developer: <lastname> <firstname> (<hexstring>)"
|
||||||
|
|
||||||
|
# (str) Name of the certificate to use for signing the release version
|
||||||
|
#ios.codesign.release = %(ios.codesign.debug)s
|
||||||
|
|
||||||
|
|
||||||
[buildozer]
|
[buildozer]
|
||||||
|
|
||||||
# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output))
|
# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output))
|
||||||
|
|
|
@ -338,7 +338,7 @@ class TargetAndroid(Target):
|
||||||
package = config.get('app', 'package.name')
|
package = config.get('app', 'package.name')
|
||||||
if package_domain:
|
if package_domain:
|
||||||
package = package_domain + '.' + package
|
package = package_domain + '.' + package
|
||||||
return package
|
return package.lower()
|
||||||
|
|
||||||
def build_package(self):
|
def build_package(self):
|
||||||
dist_dir = join(self.pa_dir, 'dist', 'default')
|
dist_dir = join(self.pa_dir, 'dist', 'default')
|
||||||
|
@ -369,6 +369,17 @@ class TargetAndroid(Target):
|
||||||
for permission in permissions:
|
for permission in permissions:
|
||||||
build_cmd += ' --permission {0}'.format(permission)
|
build_cmd += ' --permission {0}'.format(permission)
|
||||||
|
|
||||||
|
# add presplash
|
||||||
|
presplash = config.getdefault('app', 'presplash.filename', '')
|
||||||
|
if presplash:
|
||||||
|
build_cmd += ' --presplash {}'.format(join(self.buildozer.app_dir,
|
||||||
|
presplash))
|
||||||
|
|
||||||
|
# add icon
|
||||||
|
icon = config.getdefault('app', 'icon.filename', '')
|
||||||
|
if icon:
|
||||||
|
build_cmd += ' --icon {}'.format(join(self.buildozer.app_dir, icon))
|
||||||
|
|
||||||
# build only in debug right now.
|
# build only in debug right now.
|
||||||
if self.build_mode == 'debug':
|
if self.build_mode == 'debug':
|
||||||
build_cmd += ' debug'
|
build_cmd += ' debug'
|
||||||
|
|
|
@ -2,8 +2,11 @@
|
||||||
iOS target, based on kivy-ios project. (not working yet.)
|
iOS target, based on kivy-ios project. (not working yet.)
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
import plistlib
|
||||||
|
from buildozer import BuildozerCommandException
|
||||||
from buildozer.target import Target
|
from buildozer.target import Target
|
||||||
from os.path import join
|
from os.path import join, basename
|
||||||
|
from getpass import getpass
|
||||||
|
|
||||||
class TargetIos(Target):
|
class TargetIos(Target):
|
||||||
|
|
||||||
|
@ -16,15 +19,17 @@ class TargetIos(Target):
|
||||||
checkbin('Git git', 'git')
|
checkbin('Git git', 'git')
|
||||||
|
|
||||||
self.buildozer.debug('Check availability of a iPhone SDK')
|
self.buildozer.debug('Check availability of a iPhone SDK')
|
||||||
sdk = cmd('xcodebuild -showsdks | fgrep "iphoneos" | tail -n 1 | awk \'{print $2}\'')[0]
|
sdk = cmd('xcodebuild -showsdks | fgrep "iphoneos" |'
|
||||||
|
'tail -n 1 | awk \'{print $2}\'',
|
||||||
|
get_stdout=True)[0]
|
||||||
if not sdk:
|
if not sdk:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
'No iPhone SDK found. Please install at least one iOS SDK.')
|
'No iPhone SDK found. Please install at least one iOS SDK.')
|
||||||
else:
|
else:
|
||||||
print ' -> found %r' % sdk
|
self.buildozer.debug(' -> found %r' % sdk)
|
||||||
|
|
||||||
self.buildozer.debug('Check Xcode path')
|
self.buildozer.debug('Check Xcode path')
|
||||||
xcode = cmd('xcode-select -print-path')[0]
|
xcode = cmd('xcode-select -print-path', get_stdout=True)[0]
|
||||||
if not xcode:
|
if not xcode:
|
||||||
raise Exception('Unable to get xcode path')
|
raise Exception('Unable to get xcode path')
|
||||||
self.buildozer.debug(' -> found {0}'.format(xcode))
|
self.buildozer.debug(' -> found {0}'.format(xcode))
|
||||||
|
@ -39,18 +44,268 @@ class TargetIos(Target):
|
||||||
cmd('git clean -dxf', cwd=ios_dir)
|
cmd('git clean -dxf', cwd=ios_dir)
|
||||||
cmd('git pull origin master', cwd=ios_dir)
|
cmd('git pull origin master', cwd=ios_dir)
|
||||||
|
|
||||||
|
self.fruitstrap_dir = fruitstrap_dir = join(self.buildozer.platform_dir,
|
||||||
|
'fruitstrap')
|
||||||
|
if not self.buildozer.file_exists(fruitstrap_dir):
|
||||||
|
cmd('git clone git://github.com/mpurland/fruitstrap.git',
|
||||||
|
cwd=self.buildozer.platform_dir)
|
||||||
|
|
||||||
def compile_platform(self):
|
def compile_platform(self):
|
||||||
self.buildozer.cmd('tools/build-all.sh', cwd=self.ios_dir)
|
state = self.buildozer.state
|
||||||
|
is_compiled = state.get('ios.platform.compiled', '')
|
||||||
|
if not is_compiled:
|
||||||
|
self.buildozer.cmd('tools/build-all.sh', cwd=self.ios_dir)
|
||||||
|
state['ios.platform.compiled'] = '1'
|
||||||
|
|
||||||
|
if not self.buildozer.file_exists(self.fruitstrap_dir, 'fruitstrap'):
|
||||||
|
self.buildozer.cmd('make fruitstrap', cwd=self.fruitstrap_dir)
|
||||||
|
|
||||||
|
def _get_package(self):
|
||||||
|
config = self.buildozer.config
|
||||||
|
package_domain = config.getdefault('app', 'package.domain', '')
|
||||||
|
package = config.get('app', 'package.name')
|
||||||
|
if package_domain:
|
||||||
|
package = package_domain + '.' + package
|
||||||
|
return package.lower()
|
||||||
|
|
||||||
def build_package(self):
|
def build_package(self):
|
||||||
|
self._unlock_keychain()
|
||||||
|
|
||||||
# create the project
|
# create the project
|
||||||
app_name = self.buildozer.namify(self.config.get('app', 'title'))
|
app_name = self.buildozer.namify(self.buildozer.config.get('app', 'title'))
|
||||||
|
|
||||||
self.app_project_dir = join(self.ios_dir, 'app-{0}'.format(app_name))
|
self.app_project_dir = join(self.ios_dir, 'app-{0}'.format(app_name.lower()))
|
||||||
self.buildozer.cmd('tools/create-xcode-project.sh {0} {1}'.format(
|
if not self.buildozer.file_exists(self.app_project_dir):
|
||||||
app_name, self.buildozer.app_dir),
|
self.buildozer.cmd('tools/create-xcode-project.sh {0} {1}'.format(
|
||||||
cwd=self.ios_dir)
|
app_name, self.buildozer.app_dir),
|
||||||
|
cwd=self.ios_dir)
|
||||||
|
else:
|
||||||
|
self.buildozer.cmd('tools/populate-project.sh {0} {1}'.format(
|
||||||
|
app_name, self.buildozer.app_dir),
|
||||||
|
cwd=self.ios_dir)
|
||||||
|
|
||||||
|
# fix the plist
|
||||||
|
plist_fn = '{}-Info.plist'.format(app_name.lower())
|
||||||
|
plist_rfn = join(self.app_project_dir, plist_fn)
|
||||||
|
version = self.buildozer.get_version()
|
||||||
|
self.buildozer.info('Update Plist {}'.format(plist_fn))
|
||||||
|
plist = plistlib.readPlist(plist_rfn)
|
||||||
|
plist['CFBundleIdentifier'] = self._get_package()
|
||||||
|
plist['CFBundleShortVersionString'] = version
|
||||||
|
plist['CFBundleVersion'] = '{}.{}'.format(version,
|
||||||
|
self.buildozer.build_id)
|
||||||
|
|
||||||
|
# add icon
|
||||||
|
icon = self._get_icon()
|
||||||
|
if icon:
|
||||||
|
plist['CFBundleIconFiles'] = [icon]
|
||||||
|
plist['CFBundleIcons'] = {'CFBundlePrimaryIcon': {
|
||||||
|
'UIPrerenderedIcon': False, 'CFBundleIconFiles': [icon]}}
|
||||||
|
|
||||||
|
# ok, write the modified plist.
|
||||||
|
plistlib.writePlist(plist, plist_rfn)
|
||||||
|
|
||||||
|
mode = 'Debug' if self.build_mode == 'debug' else 'Release'
|
||||||
|
self.buildozer.cmd('xcodebuild -configuration {}'.format(mode),
|
||||||
|
cwd=self.app_project_dir)
|
||||||
|
ios_app_dir = 'app-{app_lower}/build/{mode}-iphoneos/{app_lower}.app'.format(
|
||||||
|
app_lower=app_name.lower(), mode=mode)
|
||||||
|
self.buildozer.state['ios:latestappdir'] = ios_app_dir
|
||||||
|
|
||||||
|
key = 'ios.codesign.{}'.format(self.build_mode)
|
||||||
|
ioscodesign = self.buildozer.config.getdefault('app', key, '')
|
||||||
|
if not ioscodesign:
|
||||||
|
self.buildozer.error('Cannot create the IPA package without'
|
||||||
|
' signature. You must fill the "{}" token.'.format(key))
|
||||||
|
return
|
||||||
|
elif ioscodesign[0] not in ('"', "'"):
|
||||||
|
ioscodesign = '"{}"'.format(ioscodesign)
|
||||||
|
|
||||||
|
ipa = join(self.buildozer.bin_dir, '{}-{}.ipa'.format(
|
||||||
|
app_name, version))
|
||||||
|
self.buildozer.cmd((
|
||||||
|
'/usr/bin/xcrun '
|
||||||
|
'-sdk iphoneos PackageApplication {ios_app_dir} '
|
||||||
|
'-o {ipa} --sign {ioscodesign} --embed '
|
||||||
|
'{ios_app_dir}/embedded.mobileprovision').format(
|
||||||
|
ioscodesign=ioscodesign, ios_app_dir=ios_app_dir,
|
||||||
|
mode=mode, ipa=ipa),
|
||||||
|
cwd=self.ios_dir)
|
||||||
|
|
||||||
|
self.buildozer.info('iOS packaging done!')
|
||||||
|
self.buildozer.info('IPA {0} available in the bin directory'.format(
|
||||||
|
basename(ipa)))
|
||||||
|
self.buildozer.state['ios:latestipa'] = ipa
|
||||||
|
self.buildozer.state['ios:latestmode'] = self.build_mode
|
||||||
|
|
||||||
|
def cmd_deploy(self, *args):
|
||||||
|
super(TargetIos, self).cmd_deploy(*args)
|
||||||
|
self._run_fruitstrap(gdb=False)
|
||||||
|
|
||||||
|
def cmd_run(self, *args):
|
||||||
|
super(TargetIos, self).cmd_run(*args)
|
||||||
|
self._run_fruitstrap(gdb=True)
|
||||||
|
|
||||||
|
def _run_fruitstrap(self, gdb=False):
|
||||||
|
state = self.buildozer.state
|
||||||
|
if 'ios:latestappdir' not in state:
|
||||||
|
self.buildozer.error(
|
||||||
|
'App not built yet. Run "debug" or "release" first.')
|
||||||
|
return
|
||||||
|
ios_app_dir = state.get('ios:latestappdir')
|
||||||
|
|
||||||
|
if gdb:
|
||||||
|
gdb_mode = '-d'
|
||||||
|
self.buildozer.info('Deploy and start the application')
|
||||||
|
else:
|
||||||
|
gdb_mode = ''
|
||||||
|
self.buildozer.info('Deploy the application')
|
||||||
|
|
||||||
|
self.buildozer.cmd('{fruitstrap} {gdb} -b {app_dir}'.format(
|
||||||
|
fruitstrap=join(self.fruitstrap_dir, 'fruitstrap'),
|
||||||
|
gdb=gdb_mode, app_dir=ios_app_dir),
|
||||||
|
cwd=self.ios_dir, show_output=True)
|
||||||
|
|
||||||
|
def _get_icon(self):
|
||||||
|
# check the icon size, must be 72x72 or 144x144
|
||||||
|
icon = self.buildozer.config.getdefault('app', 'icon.filename', '')
|
||||||
|
if not icon:
|
||||||
|
return
|
||||||
|
icon_fn = join(self.buildozer.app_dir, icon)
|
||||||
|
if not self.buildozer.file_exists(icon_fn):
|
||||||
|
self.buildozer.error('Icon {} does not exists'.format(icon_fn))
|
||||||
|
return
|
||||||
|
output = self.buildozer.cmd('file {}'.format(icon),
|
||||||
|
cwd=self.buildozer.app_dir, get_stdout=True)[0]
|
||||||
|
if not output:
|
||||||
|
self.buildozer.error('Unable to read icon {}'.format(icon_fn))
|
||||||
|
return
|
||||||
|
# output is something like:
|
||||||
|
# "data/cancel.png: PNG image data, 50 x 50, 8-bit/color RGBA,
|
||||||
|
# non-interlaced"
|
||||||
|
info = output.splitlines()[0].split(',')
|
||||||
|
fmt = info[0].split(':')[-1].strip()
|
||||||
|
if fmt != 'PNG image data':
|
||||||
|
self.buildozer.error('Only PNG icon are accepted, {} invalid'.format(icon_fn))
|
||||||
|
return
|
||||||
|
size = [int(x.strip()) for x in info[1].split('x')]
|
||||||
|
if size != [72, 72] and size != [144, 144]:
|
||||||
|
# icon cannot be used like that, it need a resize.
|
||||||
|
self.buildozer.error('Invalid PNG size, must be 72x72 or 144x144. Resampling.')
|
||||||
|
nearest_size = 144
|
||||||
|
if size[0] < 144:
|
||||||
|
nearest_size = 72
|
||||||
|
|
||||||
|
icon_basename = 'icon-{}.png'.format(nearest_size)
|
||||||
|
self.buildozer.file_copy(icon_fn, join(self.app_project_dir,
|
||||||
|
icon_basename))
|
||||||
|
self.buildozer.cmd('sips -z {0} {0} {1}'.format(nearest_size,
|
||||||
|
icon_basename), cwd=self.app_project_dir)
|
||||||
|
else:
|
||||||
|
# icon ok, use it as it.
|
||||||
|
icon_basename = 'icon-{}.png'.format(size[0])
|
||||||
|
self.buildozer.file_copy(icon_fn, join(self.app_project_dir,
|
||||||
|
icon_basename))
|
||||||
|
|
||||||
|
icon_fn = join(self.app_project_dir, icon_basename)
|
||||||
|
return icon_fn
|
||||||
|
|
||||||
|
def check_configuration_tokens(self):
|
||||||
|
errors = []
|
||||||
|
config = self.buildozer.config
|
||||||
|
identity_debug = config.getdefault('app', 'ios.codesign.debug', '')
|
||||||
|
identity_release = config.getdefault('app', 'ios.codesign.release',
|
||||||
|
identity_debug)
|
||||||
|
available_identities = self._get_available_identities()
|
||||||
|
|
||||||
|
if not identity_debug:
|
||||||
|
errors.append('[app] "ios.codesign.debug" key missing, '
|
||||||
|
'you must give a certificate name to use.')
|
||||||
|
elif identity_debug not in available_identities:
|
||||||
|
errors.append('[app] identity {} not found. '
|
||||||
|
'Check with list_identities'.format(identity_debug))
|
||||||
|
|
||||||
|
if not identity_release:
|
||||||
|
errors.append('[app] "ios.codesign.release" key missing, '
|
||||||
|
'you must give a certificate name to use.')
|
||||||
|
elif identity_release not in available_identities:
|
||||||
|
errors.append('[app] identity "{}" not found. '
|
||||||
|
'Check with list_identities'.format(identity_release))
|
||||||
|
|
||||||
|
super(TargetIos, self).check_configuration_tokens(errors)
|
||||||
|
|
||||||
|
def cmd_list_identities(self, *args):
|
||||||
|
'''List the available identities to use for signing.
|
||||||
|
'''
|
||||||
|
identities = self._get_available_identities()
|
||||||
|
print('Available identities:')
|
||||||
|
for x in identities:
|
||||||
|
print(' - {}'.format(x))
|
||||||
|
|
||||||
|
def _get_available_identities(self):
|
||||||
|
output = self.buildozer.cmd('security find-identity -v -p codesigning',
|
||||||
|
get_stdout=True)[0]
|
||||||
|
|
||||||
|
lines = output.splitlines()[:-1]
|
||||||
|
lines = ['"{}"'.format(x.split('"')[1]) for x in lines]
|
||||||
|
return lines
|
||||||
|
|
||||||
|
def _unlock_keychain(self):
|
||||||
|
password_file = join(self.buildozer.buildozer_dir, '.ioscodesign')
|
||||||
|
password = None
|
||||||
|
if self.buildozer.file_exists(password_file):
|
||||||
|
with open(password_file) as fd:
|
||||||
|
password = fd.read()
|
||||||
|
|
||||||
|
if not password:
|
||||||
|
# no password available, try to unlock anyway...
|
||||||
|
error = self.buildozer.cmd('security unlock-keychain -u',
|
||||||
|
break_on_error=False)[2]
|
||||||
|
if not error:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# password available, try to unlock
|
||||||
|
error = self.buildozer.cmd('security unlock-keychain -p {}'.format(
|
||||||
|
password), break_on_error=False, sensible=True)[2]
|
||||||
|
if not error:
|
||||||
|
return
|
||||||
|
|
||||||
|
# we need the password to unlock.
|
||||||
|
correct = False
|
||||||
|
attempt = 3
|
||||||
|
while attempt:
|
||||||
|
attempt -= 1
|
||||||
|
password = getpass('Password to unlock the default keychain:')
|
||||||
|
error = self.buildozer.cmd('security unlock-keychain -p "{}"'.format(
|
||||||
|
password), break_on_error=False, sensible=True)[2]
|
||||||
|
if not error:
|
||||||
|
correct = True
|
||||||
|
break
|
||||||
|
self.error('Invalid keychain password')
|
||||||
|
|
||||||
|
if not correct:
|
||||||
|
self.error('Unable to unlock the keychain, exiting.')
|
||||||
|
raise BuildozerCommandException()
|
||||||
|
|
||||||
|
# maybe user want to save it for further reuse?
|
||||||
|
print(
|
||||||
|
'The keychain password can be saved in the build directory\n'
|
||||||
|
'As soon as the build directory will be cleaned, '
|
||||||
|
'the password will be erased.')
|
||||||
|
|
||||||
|
save = None
|
||||||
|
while save is None:
|
||||||
|
q = raw_input('Do you want to save the password (Y/n): ')
|
||||||
|
if q in ('', 'Y'):
|
||||||
|
save = True
|
||||||
|
elif q == 'n':
|
||||||
|
save = False
|
||||||
|
else:
|
||||||
|
print('Invalid answer!')
|
||||||
|
|
||||||
|
if save:
|
||||||
|
with open(password_file, 'wb') as fd:
|
||||||
|
fd.write(password)
|
||||||
|
|
||||||
def get_target(buildozer):
|
def get_target(buildozer):
|
||||||
return TargetIos(buildozer)
|
return TargetIos(buildozer)
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -13,6 +13,6 @@ setup(
|
||||||
'buildozer',
|
'buildozer',
|
||||||
'buildozer.targets'],
|
'buildozer.targets'],
|
||||||
package_data={'buildozer': ['default.spec']},
|
package_data={'buildozer': ['default.spec']},
|
||||||
scripts=['tools/buildozer'],
|
scripts=['tools/buildozer', 'tools/buildozer-remote'],
|
||||||
description='Generic Python packager for Android / iOS and Desktop'
|
description='Generic Python packager for Android / iOS and Desktop'
|
||||||
)
|
)
|
||||||
|
|
5
tools/buildozer-remote
Executable file
5
tools/buildozer-remote
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/env python2.7
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from buildozer import run_remote
|
||||||
|
run_remote()
|
Loading…
Reference in a new issue