This commit is contained in:
tshirtman 2013-01-26 19:29:30 +01:00
commit 8e96078ad1
6 changed files with 586 additions and 24 deletions

View file

@ -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

View file

@ -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))

View file

@ -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'

View file

@ -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)

View file

@ -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
View file

@ -0,0 +1,5 @@
#!/usr/bin/env python2.7
if __name__ == '__main__':
from buildozer import run_remote
run_remote()