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'
|
||||
|
||||
import shelve
|
||||
import zipfile
|
||||
import sys
|
||||
import fcntl
|
||||
import os
|
||||
import re
|
||||
import shelve
|
||||
import socket
|
||||
import sys
|
||||
import zipfile
|
||||
from select import select
|
||||
from sys import stdout, stderr, exit
|
||||
from sys import stdout, stderr, stdin, exit
|
||||
from urllib import urlretrieve
|
||||
from re import search
|
||||
from ConfigParser import SafeConfigParser
|
||||
|
@ -29,6 +30,14 @@ from os import environ, unlink, rename, walk, sep, listdir, makedirs
|
|||
from copy import copy
|
||||
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"
|
||||
COLOR_SEQ = "\033[1;{0}m"
|
||||
BOLD_SEQ = "\033[1m"
|
||||
|
@ -53,6 +62,7 @@ class Buildozer(object):
|
|||
self.environ = {}
|
||||
self.specfilename = filename
|
||||
self.state = None
|
||||
self.build_id = None
|
||||
self.config = SafeConfigParser()
|
||||
self.config.getlist = self._get_config_list
|
||||
self.config.getdefault = self._get_config_default
|
||||
|
@ -122,7 +132,15 @@ class Buildozer(object):
|
|||
if hasattr(self.target, '_build_done'):
|
||||
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.info('Package the application')
|
||||
|
@ -187,8 +205,16 @@ class Buildozer(object):
|
|||
show_output = kwargs.pop('show_output')
|
||||
get_stdout = kwargs.pop('get_stdout', 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')))
|
||||
|
||||
# open the process
|
||||
|
@ -229,14 +255,14 @@ class Buildozer(object):
|
|||
stderr.flush()
|
||||
|
||||
process.communicate()
|
||||
if process.returncode != 0:
|
||||
if process.returncode != 0 and break_on_error:
|
||||
self.error('Command failed: {0}'.format(command))
|
||||
raise BuildozerCommandException()
|
||||
if ret_stdout:
|
||||
ret_stdout = ''.join(ret_stdout)
|
||||
if ret_stderr:
|
||||
ret_stderr = ''.join(ret_stderr)
|
||||
return (ret_stdout, ret_stderr)
|
||||
return (ret_stdout, ret_stderr, process.returncode)
|
||||
|
||||
def check_configuration_tokens(self):
|
||||
'''Ensure the spec file is 'correct'.
|
||||
|
@ -384,6 +410,13 @@ class Buildozer(object):
|
|||
self.debug('Rename {0} to {1}'.format(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):
|
||||
if archive.endswith('.tgz') or archive.endswith('.tar.gz'):
|
||||
# XXX tarfile doesn't work for NDK-r8c :(
|
||||
|
@ -467,11 +500,18 @@ class Buildozer(object):
|
|||
raise Exception('Missing version or version.regex + version.filename')
|
||||
|
||||
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', '.'))
|
||||
include_exts = self.config.getlist('app', 'source.include_exts', '')
|
||||
exclude_exts = self.config.getlist('app', 'source.exclude_exts', '')
|
||||
app_dir = self.app_dir
|
||||
|
||||
self.debug('Copy application source from {}'.format(source_dir))
|
||||
|
||||
rmtree(self.app_dir)
|
||||
|
||||
for root, dirs, files in walk(source_dir):
|
||||
|
@ -505,11 +545,13 @@ class Buildozer(object):
|
|||
self.debug('Copy {0}'.format(sfn))
|
||||
copyfile(sfn, rfn)
|
||||
|
||||
def _copy_application_libs(self):
|
||||
# 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
|
||||
main_py = join(app_dir, 'main.py')
|
||||
main_py = join(self.app_dir, 'main.py')
|
||||
if not self.file_exists(main_py):
|
||||
self.error('Unable to patch main_py to add applibs directory.')
|
||||
return
|
||||
|
@ -564,6 +606,13 @@ class Buildozer(object):
|
|||
def global_cache_dir(self):
|
||||
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'])
|
||||
yield target, m
|
||||
except:
|
||||
raise
|
||||
pass
|
||||
|
||||
def usage(self):
|
||||
|
@ -650,7 +700,7 @@ class Buildozer(object):
|
|||
self.usage()
|
||||
exit(0)
|
||||
|
||||
if args == '--version':
|
||||
if arg == '--version':
|
||||
print 'Buildozer {0}'.format(__version__)
|
||||
exit(0)
|
||||
|
||||
|
@ -725,6 +775,224 @@ class Buildozer(object):
|
|||
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():
|
||||
try:
|
||||
Buildozer().run_command(sys.argv[1:])
|
||||
|
@ -733,3 +1001,8 @@ def run():
|
|||
# command failed.
|
||||
pass
|
||||
|
||||
def run_remote():
|
||||
try:
|
||||
BuildozerRemote().run_command(sys.argv[1:])
|
||||
except BuildozerCommandException:
|
||||
pass
|
||||
|
|
|
@ -13,7 +13,7 @@ package.domain = org.test
|
|||
source.dir = .
|
||||
|
||||
# (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)
|
||||
#source.exclude_exts = spec
|
||||
|
@ -28,6 +28,12 @@ version.filename = %(source.dir)s/main.py
|
|||
# (list) Application requirements
|
||||
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
|
||||
#
|
||||
|
@ -56,6 +62,18 @@ requirements = twisted,kivy
|
|||
# (str) Android entry point, default is ok for Kivy-based app
|
||||
#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]
|
||||
|
||||
# (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')
|
||||
if package_domain:
|
||||
package = package_domain + '.' + package
|
||||
return package
|
||||
return package.lower()
|
||||
|
||||
def build_package(self):
|
||||
dist_dir = join(self.pa_dir, 'dist', 'default')
|
||||
|
@ -369,6 +369,17 @@ class TargetAndroid(Target):
|
|||
for permission in permissions:
|
||||
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.
|
||||
if self.build_mode == 'debug':
|
||||
build_cmd += ' debug'
|
||||
|
|
|
@ -2,8 +2,11 @@
|
|||
iOS target, based on kivy-ios project. (not working yet.)
|
||||
'''
|
||||
|
||||
import plistlib
|
||||
from buildozer import BuildozerCommandException
|
||||
from buildozer.target import Target
|
||||
from os.path import join
|
||||
from os.path import join, basename
|
||||
from getpass import getpass
|
||||
|
||||
class TargetIos(Target):
|
||||
|
||||
|
@ -16,15 +19,17 @@ class TargetIos(Target):
|
|||
checkbin('Git git', 'git')
|
||||
|
||||
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:
|
||||
raise Exception(
|
||||
'No iPhone SDK found. Please install at least one iOS SDK.')
|
||||
else:
|
||||
print ' -> found %r' % sdk
|
||||
self.buildozer.debug(' -> found %r' % sdk)
|
||||
|
||||
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:
|
||||
raise Exception('Unable to get xcode path')
|
||||
self.buildozer.debug(' -> found {0}'.format(xcode))
|
||||
|
@ -39,18 +44,268 @@ class TargetIos(Target):
|
|||
cmd('git clean -dxf', 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):
|
||||
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):
|
||||
self._unlock_keychain()
|
||||
|
||||
# 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.buildozer.cmd('tools/create-xcode-project.sh {0} {1}'.format(
|
||||
app_name, self.buildozer.app_dir),
|
||||
cwd=self.ios_dir)
|
||||
self.app_project_dir = join(self.ios_dir, 'app-{0}'.format(app_name.lower()))
|
||||
if not self.buildozer.file_exists(self.app_project_dir):
|
||||
self.buildozer.cmd('tools/create-xcode-project.sh {0} {1}'.format(
|
||||
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):
|
||||
return TargetIos(buildozer)
|
||||
|
|
2
setup.py
2
setup.py
|
@ -13,6 +13,6 @@ setup(
|
|||
'buildozer',
|
||||
'buildozer.targets'],
|
||||
package_data={'buildozer': ['default.spec']},
|
||||
scripts=['tools/buildozer'],
|
||||
scripts=['tools/buildozer', 'tools/buildozer-remote'],
|
||||
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