class Buildozer(object): standard_cmds = ('clean', 'update', 'debug', 'release', 'deploy', 'run', 'serve') def __init__(self, filename='buildozer.spec', target=None): super(Buildozer, self).__init__() self.log_level = 1 self.environ = {} self.specfilename = filename self.state = None self.build_id = None self.config_profile = '' self.config = SafeConfigParser(allow_no_value=True) self.config.optionxform = lambda value: value self.config.getlist = self._get_config_list self.config.getlistvalues = self._get_config_list_values self.config.getdefault = self._get_config_default self.config.getbooldefault = self._get_config_bool if exists(filename): self.config.read(filename) self.check_configuration_tokens() try: self.log_level = int( self.config.getdefault('buildozer', 'log_level', '1')) except: pass self.targetname = None self.target = None if target: self.set_target(target) def set_target(self, target): '''Set the target to use (one of buildozer.targets, such as "android") ''' self.targetname = target m = __import__('buildozer.targets.{0}'.format(target), fromlist=['buildozer']) self.target = m.get_target(self) self.check_build_layout() self.check_configuration_tokens() def prepare_for_build(self): '''Prepare the build. ''' assert (self.target is not None) if hasattr(self.target, '_build_prepared'): return self.info('Preparing build') self.info('Check requirements for {0}'.format(self.targetname)) self.target.check_requirements() self.info('Install platform') self.target.install_platform() self.info('Check application requirements') self.check_application_requirements() self.info('Compile platform') self.target.compile_platform() # flag to prevent multiple build self.target._build_prepared = True def build(self): '''Do the build. The target can set build_mode to 'release' or 'debug' before calling this method. (:meth:`prepare_for_build` must have been call before.) ''' assert (self.target is not None) assert (hasattr(self.target, '_build_prepared')) if hasattr(self.target, '_build_done'): return # 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') self.target.build_package() # flag to prevent multiple build self.target._build_done = True # # Log functions # def log(self, level, msg): if level > self.log_level: return if USE_COLOR: color = COLOR_SEQ.format(30 + LOG_LEVELS_C[level]) print ''.join((RESET_SEQ, color, '# ', msg, RESET_SEQ)) else: print LOG_LEVELS_T[level], msg def debug(self, msg): self.log(2, msg) def info(self, msg): self.log(1, msg) def error(self, msg): self.log(0, msg) # # Internal check methods # def checkbin(self, msg, fn): self.debug('Search for {0}'.format(msg)) if exists(fn): return realpath(fn) for dn in environ['PATH'].split(':'): rfn = realpath(join(dn, fn)) if exists(rfn): self.debug(' -> found at {0}'.format(rfn)) return rfn raise Exception(msg + 'not found') def cmd(self, command, **kwargs): #print ' '.join(['{0}={1}'.format(*args) for args in # self.environ.iteritems()]) # prepare the environ, based on the system + our own env env = copy(environ) env.update(self.environ) # prepare the process kwargs.setdefault('env', env) kwargs.setdefault('stdout', PIPE) kwargs.setdefault('stderr', PIPE) kwargs.setdefault('close_fds', True) kwargs.setdefault('shell', True) kwargs.setdefault('show_output', self.log_level > 1) 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) 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 process = Popen(command, **kwargs) # prepare fds fd_stdout = process.stdout.fileno() fd_stderr = process.stderr.fileno() fcntl.fcntl(fd_stdout, fcntl.F_SETFL, fcntl.fcntl(fd_stdout, fcntl.F_GETFL) | os.O_NONBLOCK) fcntl.fcntl(fd_stderr, fcntl.F_SETFL, fcntl.fcntl(fd_stderr, fcntl.F_GETFL) | os.O_NONBLOCK) ret_stdout = [] if get_stdout else None ret_stderr = [] if get_stderr else None while True: readx = select([fd_stdout, fd_stderr], [], [])[0] if fd_stdout in readx: chunk = process.stdout.read() if chunk == '': break if get_stdout: ret_stdout.append(chunk) if show_output: stdout.write(chunk) if fd_stderr in readx: chunk = process.stderr.read() if chunk == '': break if get_stderr: ret_stderr.append(chunk) if show_output: stderr.write(chunk) stdout.flush() stderr.flush() process.communicate() 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, process.returncode) def check_configuration_tokens(self): '''Ensure the spec file is 'correct'. ''' self.info('Check configuration tokens') get = self.config.getdefault errors = [] adderror = errors.append if not get('app', 'title', ''): adderror('[app] "title" is missing') if not get('app', 'package.name', ''): adderror('[app] "package.name" is missing') if not get('app', 'source.dir', ''): adderror('[app] "source.dir" is missing') version = get('app', 'version', '') version_regex = get('app', 'version.regex', '') if not version and not version_regex: adderror('[app] One of "version" or "version.regex" must be set') if version and version_regex: adderror('[app] Conflict between "version" and "version.regex"' ', only one can be used.') if version_regex and not get('app', 'version.filename', ''): adderror('[app] "version.filename" is missing' ', required by "version.regex"') orientation = get('app', 'orientation', 'landscape') if orientation not in ('landscape', 'portrait', 'all'): adderror('[app] "orientation" have an invalid value') if errors: self.error('{0} error(s) found in the buildozer.spec'.format( len(errors))) for error in errors: print error exit(1) def check_build_layout(self): '''Ensure the build (local and global) directory layout and files are ready. ''' self.info('Ensure build layout') if not exists(self.specfilename): print 'No {0} found in the current directory. Abandon.'.format( self.specfilename) exit(1) # create global dir self.mkdir(self.global_buildozer_dir) self.mkdir(self.global_cache_dir) # create local dir specdir = dirname(self.specfilename) self.mkdir(join(specdir, '.buildozer')) self.mkdir(join(specdir, 'bin')) self.mkdir(self.applibs_dir) self.state = shelve.open(join(self.buildozer_dir, 'state.db')) if self.targetname: target = self.targetname self.mkdir(join(self.global_platform_dir, target, 'platform')) self.mkdir(join(specdir, '.buildozer', target, 'platform')) self.mkdir(join(specdir, '.buildozer', target, 'app')) def check_application_requirements(self): '''Ensure the application requirements are all available and ready to be packaged as well. ''' requirements = self.config.getlist('app', 'requirements', '') target_available_packages = self.target.get_available_packages() # remove all the requirements that the target can compile onlyname = lambda x: x.split('==')[0] requirements = [ x for x in requirements if onlyname(x) not in target_available_packages ] # did we already installed the libs ? if exists(self.applibs_dir) and \ self.state.get('cache.applibs', '') == requirements: self.debug('Application requirements already installed, pass') return # recreate applibs self.rmdir(self.applibs_dir) self.mkdir(self.applibs_dir) # ok now check the availability of all requirements for requirement in requirements: self._install_application_requirement(requirement) # everything goes as expected, save this state! self.state['cache.applibs'] = requirements def _install_application_requirement(self, module): self._ensure_virtualenv() # resetup distribute, just in case self.debug('Install distribute') self.cmd( 'curl http://python-distribute.org/distribute_setup.py | venv/bin/python', get_stdout=True, cwd=self.buildozer_dir) self.debug('Install requirement {} in virtualenv'.format(module)) self.cmd('pip-2.7 install --download-cache={} --target={} {}'.format( self.global_cache_dir, self.applibs_dir, module), env=self.env_venv, cwd=self.buildozer_dir) def _ensure_virtualenv(self): if hasattr(self, 'venv'): return self.venv = join(self.buildozer_dir, 'venv') if not self.file_exists(self.venv): self.cmd('virtualenv-2.7 --python=python2.7 ./venv', cwd=self.buildozer_dir) # read virtualenv output and parse it output = self.cmd('bash -c "source venv/bin/activate && env"', get_stdout=True, cwd=self.buildozer_dir) self.env_venv = copy(self.environ) for line in output[0].splitlines(): args = line.split('=', 1) if len(args) != 2: continue key, value = args if key in ('VIRTUAL_ENV', 'PATH'): self.env_venv[key] = value if 'PYTHONHOME' in self.env_venv: del self.env_venv['PYTHONHOME'] # ensure any sort of compilation will fail self.env_venv['CC'] = '/bin/false' self.env_venv['CXX'] = '/bin/false' def mkdir(self, dn): if exists(dn): return self.debug('Create directory {0}'.format(dn)) makedirs(dn) def rmdir(self, dn): if not exists(dn): return self.debug('Remove directory and subdirectory {}'.format(dn)) rmtree(dn) def file_exists(self, *args): return exists(join(*args)) def file_rename(self, source, target, cwd=None): if cwd: source = join(cwd, source) target = join(cwd, target) 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 :( #tf = tarfile.open(archive, 'r:*') #tf.extractall(path=cwd) #tf.close() self.cmd('tar xzf {0}'.format(archive), cwd=cwd) return if archive.endswith('.tbz2') or archive.endswith('.tar.bz2'): # XXX same as before self.cmd('tar xjf {0}'.format(archive), cwd=cwd) return if archive.endswith('.zip'): archive = join(cwd, archive) zf = zipfile.ZipFile(archive) zf.extractall(path=cwd) zf.close() return raise Exception('Unhandled extraction for type {0}'.format(archive)) def file_copytree(self, src, dest): print 'copy {} to {}'.format(src, dest) if os.path.isdir(src): if not os.path.isdir(dest): os.makedirs(dest) files = os.listdir(src) for f in files: self.file_copytree(os.path.join(src, f), os.path.join(dest, f)) else: copyfile(src, dest) def clean_platform(self): self.info('Clean the platform build directory') if not exists(self.platform_dir): return rmtree(self.platform_dir) def download(self, url, filename, cwd=None): def report_hook(index, blksize, size): if size <= 0: progression = '{0} bytes'.format(index * blksize) else: progression = '{0:.2f}%'.format(index * blksize * 100. / float(size)) print '- Download', progression, '\r', stdout.flush() url = url + filename if cwd: filename = join(cwd, filename) if self.file_exists(filename): unlink(filename) self.debug('Downloading {0}'.format(url)) urlretrieve(url, filename, report_hook) return filename def get_version(self): c = self.config has_version = c.has_option('app', 'version') has_regex = c.has_option('app', 'version.regex') has_filename = c.has_option('app', 'version.filename') # version number specified if has_version: if has_regex or has_filename: raise Exception( 'version.regex and version.filename conflict with version') return c.get('app', 'version') # search by regex if has_regex or has_filename: if has_regex and not has_filename: raise Exception('version.filename is missing') if has_filename and not has_regex: raise Exception('version.regex is missing') fn = c.get('app', 'version.filename') with open(fn) as fd: data = fd.read() regex = c.get('app', 'version.regex') match = search(regex, data) if not match: raise Exception('Unable to find capture version in {0}\n' ' (looking for `{1}`)'.format(fn, regex)) version = match.groups()[0] self.debug('Captured version: {0}'.format(version)) return version raise Exception('Missing version or version.regex + version.filename') def build_application(self): self._copy_application_sources() self._copy_application_libs() self._add_sitecustomize() def _copy_application_sources(self): # XXX clean the inclusion/exclusion algo. 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', '') exclude_dirs = self.config.getlist('app', 'source.exclude_dirs', '') exclude_patterns = self.config.getlist('app', 'source.exclude_patterns', '') 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): # avoid hidden directory if True in [x.startswith('.') for x in root.split(sep)]: continue # need to have sort-of normalization. Let's say you want to exclude # image directory but not images, the filtered_root must have a / at # the end, same for the exclude_dir. And then we can safely compare filtered_root = root[len(source_dir) + 1:].lower() if filtered_root: filtered_root += '/' # manual exclude_dirs approach is_excluded = False for exclude_dir in exclude_dirs: if exclude_dir[-1] != '/': exclude_dir += '/' if filtered_root.startswith(exclude_dir): is_excluded = True break if is_excluded: continue # pattern matching for pattern in exclude_patterns: if fnmatch(filtered_root, pattern): is_excluded = True break if is_excluded: continue for fn in files: # avoid hidden files if fn.startswith('.'): continue # exclusion by pattern matching is_excluded = False dfn = fn.lower() if filtered_root: dfn = join(filtered_root, fn) for pattern in exclude_patterns: if fnmatch(dfn, pattern): is_excluded = True break if is_excluded: continue # filter based on the extension # TODO more filters basename, ext = splitext(fn) if ext: ext = ext[1:] if include_exts and ext not in include_exts: continue if exclude_exts and ext in exclude_exts: continue sfn = join(root, fn) rfn = realpath(join(app_dir, root[len(source_dir) + 1:], fn)) # ensure the directory exists dfn = dirname(rfn) self.mkdir(dfn) # copy! self.debug('Copy {0}'.format(sfn)) copyfile(sfn, rfn) def _copy_application_libs(self): # copy also the libs copytree(self.applibs_dir, join(self.app_dir, '_applibs')) def _add_sitecustomize(self): copyfile(join(dirname(__file__), 'sitecustomize.py'), join(self.app_dir, 'sitecustomize.py')) main_py = join(self.app_dir, 'service', 'main.py') if not self.file_exists(main_py): #self.error('Unable to patch main_py to add applibs directory.') return header = ('import sys, os; ' 'sys.path = [os.path.join(os.getcwd(),' '"..", "_applibs")] + sys.path\n') with open(main_py, 'rb') as fd: data = fd.read() data = header + data with open(main_py, 'wb') as fd: fd.write(data) self.info('Patched service/main.py to include applibs') def namify(self, name): '''Return a "valid" name from a name with lot of invalid chars (allowed characters: a-z, A-Z, 0-9, -, _) ''' return re.sub('[^a-zA-Z0-9_\-]', '_', name) @property def root_dir(self): return realpath(join(dirname(self.specfilename))) @property def buildozer_dir(self): return join(self.root_dir, '.buildozer') @property def bin_dir(self): return join(self.root_dir, 'bin') @property def platform_dir(self): return join(self.buildozer_dir, self.targetname, 'platform') @property def app_dir(self): return join(self.buildozer_dir, self.targetname, 'app') @property def applibs_dir(self): return join(self.buildozer_dir, 'applibs') @property def global_buildozer_dir(self): return join(expanduser('~'), '.buildozer') @property def global_platform_dir(self): return join(self.global_buildozer_dir, self.targetname, 'platform') @property def global_packages_dir(self): return join(self.global_buildozer_dir, self.targetname, 'packages') @property 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) # # command line invocation # def targets(self): for fn in listdir(join(dirname(__file__), 'targets')): if fn.startswith('.') or fn.startswith('__'): continue if not fn.endswith('.py'): continue target = fn[:-3] try: m = __import__('buildozer.targets.{0}'.format(target), fromlist=['buildozer']) yield target, m except: raise pass def usage(self): print 'Usage:' print ' buildozer [--profile <name>] [--verbose] [target] <command>...' print ' buildozer --version' print print 'Available targets:' targets = list(self.targets()) for target, m in targets: doc = m.__doc__.strip().splitlines()[0].strip() print ' {0:<18} {1}'.format(target, doc) print print 'Global commands (without target):' cmds = [x for x in dir(self) if x.startswith('cmd_')] for cmd in cmds: name = cmd[4:] meth = getattr(self, cmd) doc = [x for x in meth.__doc__.strip().splitlines()][0].strip() print ' {0:<18} {1}'.format(name, doc) print print 'Target commands:' print ' clean Clean the target environment' print ' update Update the target dependencies' print ' debug Build the application in debug mode' print ' release Build the application in release mode' print ' deploy Deploy the application on the device' print ' run Run the application on the device' print ' serve Serve the bin directory via SimpleHTTPServer' for target, m in targets: mt = m.get_target(self) commands = mt.get_custom_commands() if not commands: continue print print 'Target "{0}" commands:'.format(target) for command, doc in commands: doc = doc.strip().splitlines()[0].strip() print ' {0:<18} {1}'.format(command, doc) print def run_default(self): self.check_build_layout() if 'buildozer:defaultcommand' not in self.state: print 'No default command set.' print 'Use "buildozer setdefault <command args...>"' print 'Use "buildozer help" for a list of all commands"' exit(1) cmd = self.state['buildozer:defaultcommand'] self.run_command(cmd) 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 elif arg in ('-h', '--help'): self.usage() exit(0) elif arg in ('-p', '--profile'): self.config_profile = args.pop(0) elif arg == '--version': print 'Buildozer {0}'.format(__version__) exit(0) self._merge_config_profile() if not args: self.run_default() return command, args = args[0], args[1:] cmd = 'cmd_{0}'.format(command) # internal commands ? if hasattr(self, cmd): getattr(self, cmd)(*args) return # maybe it's a target? targets = [x[0] for x in self.targets()] if command not in targets: print 'Unknown command/target', command exit(1) self.set_target(command) self.target.run_commands(args) def cmd_init(self, *args): '''Create a initial buildozer.spec in the current directory ''' if exists('buildozer.spec'): print 'ERROR: You already have a buildozer.spec file.' exit(1) copyfile(join(dirname(__file__), 'default.spec'), 'buildozer.spec') print 'File buildozer.spec created, ready to customize!' def cmd_clean(self, *args): '''Clean the whole Buildozer environment. ''' pass def cmd_help(self, *args): '''Show the Buildozer help. ''' self.usage() def cmd_setdefault(self, *args): '''Set the default command to do when to arguments are given ''' self.check_build_layout() self.state['buildozer:defaultcommand'] = args def cmd_version(self, *args): '''Show the Buildozer version ''' print 'Buildozer {0}'.format(__version__) def cmd_serve(self, *args): '''Serve the bin directory via SimpleHTTPServer ''' os.chdir(self.bin_dir) handler = SimpleHTTPServer.SimpleHTTPRequestHandler httpd = SocketServer.TCPServer(("", SIMPLE_HTTP_SERVER_PORT), handler) print("Serving via HTTP at port {}".format(SIMPLE_HTTP_SERVER_PORT)) print("Press Ctrl+c to quit serving.") httpd.serve_forever() # # Private # def _merge_config_profile(self): profile = self.config_profile if not profile: return for section in self.config.sections(): # extract the profile part from the section name # example: [app@default,hd] parts = section.split('@', 1) if len(parts) < 2: continue # create a list that contain all the profiles of the current section # ['default', 'hd'] section_base, section_profiles = parts section_profiles = section_profiles.split(',') if profile not in section_profiles: continue # the current profile is one available in the section # merge with the general section, or make it one. if not self.config.has_section(section_base): self.config.add_section(section_base) for name, value in self.config.items(section): print 'merged ({}, {}) into {} (profile is {})'.format( name, value, section_base, profile) self.config.set(section_base, name, value) def _get_config_list_values(self, *args, **kwargs): kwargs['with_values'] = True return self._get_config_list(*args, **kwargs) def _get_config_list(self, section, token, default=None, with_values=False): # monkey-patch method for ConfigParser # get a key as a list of string, seperated from the comma # if a section:token is defined, let's use the content as a list. l_section = '{}:{}'.format(section, token) if self.config.has_section(l_section): values = self.config.options(l_section) if with_values: return [ '{}={}'.format(key, self.config.get(l_section, key)) for key in values ] else: return [x.strip() for x in values] values = self.config.getdefault(section, token, '') if not values: return default values = values.split(',') if not values: return default return [x.strip() for x in values] def _get_config_default(self, section, token, default=None): # monkey-patch method for ConfigParser # get a key in a section, or the default if not self.config.has_section(section): return default if not self.config.has_option(section, token): return default return self.config.get(section, token) def _get_config_bool(self, section, token, default=False): # monkey-patch method for ConfigParser # get a key in a section, or the default if not self.config.has_section(section): return default if not self.config.has_option(section, token): return default return self.config.getboolean(section, token)
class Buildozer(object): standard_cmds = ("clean", "update", "debug", "release", "deploy", "run", "serve") def __init__(self, filename="buildozer.spec", target=None): super(Buildozer, self).__init__() self.log_level = 1 self.environ = {} self.specfilename = filename self.state = None self.build_id = None self.config_profile = "" self.config = SafeConfigParser(allow_no_value=True) self.config.optionxform = lambda value: value self.config.getlist = self._get_config_list self.config.getlistvalues = self._get_config_list_values self.config.getdefault = self._get_config_default self.config.getbooldefault = self._get_config_bool if exists(filename): self.config.read(filename) self.check_configuration_tokens() # Check all section/tokens for env vars, and replace the # config value if a suitable env var exists. set_config_from_envs(self.config) try: self.log_level = int(self.config.getdefault("buildozer", "log_level", "1")) except: pass self.targetname = None self.target = None if target: self.set_target(target) def set_target(self, target): """Set the target to use (one of buildozer.targets, such as "android") """ self.targetname = target m = __import__("buildozer.targets.{0}".format(target), fromlist=["buildozer"]) self.target = m.get_target(self) self.check_build_layout() self.check_configuration_tokens() def prepare_for_build(self): """Prepare the build. """ assert self.target is not None if hasattr(self.target, "_build_prepared"): return self.info("Preparing build") self.info("Check requirements for {0}".format(self.targetname)) self.target.check_requirements() self.info("Install platform") self.target.install_platform() self.info("Check application requirements") self.check_application_requirements() self.info("Compile platform") self.target.compile_platform() # flag to prevent multiple build self.target._build_prepared = True def build(self): """Do the build. The target can set build_mode to 'release' or 'debug' before calling this method. (:meth:`prepare_for_build` must have been call before.) """ assert self.target is not None assert hasattr(self.target, "_build_prepared") if hasattr(self.target, "_build_done"): return # 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") self.target.build_package() # flag to prevent multiple build self.target._build_done = True # # Log functions # def log(self, level, msg): if level > self.log_level: return if USE_COLOR: color = COLOR_SEQ.format(30 + LOG_LEVELS_C[level]) print "".join((RESET_SEQ, color, "# ", msg, RESET_SEQ)) else: print LOG_LEVELS_T[level], msg def debug(self, msg): self.log(2, msg) def info(self, msg): self.log(1, msg) def error(self, msg): self.log(0, msg) # # Internal check methods # def checkbin(self, msg, fn): self.debug("Search for {0}".format(msg)) if exists(fn): return realpath(fn) for dn in environ["PATH"].split(":"): rfn = realpath(join(dn, fn)) if exists(rfn): self.debug(" -> found at {0}".format(rfn)) return rfn raise Exception(msg + "not found") def cmd(self, command, **kwargs): # print ' '.join(['{0}={1}'.format(*args) for args in # self.environ.iteritems()]) # prepare the environ, based on the system + our own env env = copy(environ) env.update(self.environ) # prepare the process kwargs.setdefault("env", env) kwargs.setdefault("stdout", PIPE) kwargs.setdefault("stderr", PIPE) kwargs.setdefault("close_fds", True) kwargs.setdefault("shell", True) kwargs.setdefault("show_output", self.log_level > 1) 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) 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 process = Popen(command, **kwargs) # prepare fds fd_stdout = process.stdout.fileno() fd_stderr = process.stderr.fileno() fcntl.fcntl(fd_stdout, fcntl.F_SETFL, fcntl.fcntl(fd_stdout, fcntl.F_GETFL) | os.O_NONBLOCK) fcntl.fcntl(fd_stderr, fcntl.F_SETFL, fcntl.fcntl(fd_stderr, fcntl.F_GETFL) | os.O_NONBLOCK) ret_stdout = [] if get_stdout else None ret_stderr = [] if get_stderr else None while True: readx = select([fd_stdout, fd_stderr], [], [])[0] if fd_stdout in readx: chunk = process.stdout.read() if chunk == "": break if get_stdout: ret_stdout.append(chunk) if show_output: stdout.write(chunk) if fd_stderr in readx: chunk = process.stderr.read() if chunk == "": break if get_stderr: ret_stderr.append(chunk) if show_output: stderr.write(chunk) stdout.flush() stderr.flush() process.communicate() 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, process.returncode) def check_configuration_tokens(self): """Ensure the spec file is 'correct'. """ self.info("Check configuration tokens") get = self.config.getdefault errors = [] adderror = errors.append if not get("app", "title", ""): adderror('[app] "title" is missing') if not get("app", "source.dir", ""): adderror('[app] "source.dir" is missing') package_name = get("app", "package.name", "") if not package_name: adderror('[app] "package.name" is missing') elif package_name[0] in map(str, range(10)): adderror('[app] "package.name" may not start with a number.') version = get("app", "version", "") version_regex = get("app", "version.regex", "") if not version and not version_regex: adderror('[app] One of "version" or "version.regex" must be set') if version and version_regex: adderror('[app] Conflict between "version" and "version.regex"' ", only one can be used.") if version_regex and not get("app", "version.filename", ""): adderror('[app] "version.filename" is missing' ', required by "version.regex"') orientation = get("app", "orientation", "landscape") if orientation not in ("landscape", "portrait", "all"): adderror('[app] "orientation" have an invalid value') if errors: self.error("{0} error(s) found in the buildozer.spec".format(len(errors))) for error in errors: print error exit(1) def check_build_layout(self): """Ensure the build (local and global) directory layout and files are ready. """ self.info("Ensure build layout") if not exists(self.specfilename): print "No {0} found in the current directory. Abandon.".format(self.specfilename) exit(1) # create global dir self.mkdir(self.global_buildozer_dir) self.mkdir(self.global_cache_dir) # create local dir specdir = dirname(self.specfilename) self.mkdir(join(specdir, ".buildozer")) self.mkdir(join(specdir, "bin")) self.mkdir(self.applibs_dir) self.state = shelve.open(join(self.buildozer_dir, "state.db")) if self.targetname: target = self.targetname self.mkdir(join(self.global_platform_dir, target, "platform")) self.mkdir(join(specdir, ".buildozer", target, "platform")) self.mkdir(join(specdir, ".buildozer", target, "app")) def check_application_requirements(self): """Ensure the application requirements are all available and ready to be packaged as well. """ requirements = self.config.getlist("app", "requirements", "") target_available_packages = self.target.get_available_packages() # remove all the requirements that the target can compile onlyname = lambda x: x.split("==")[0] requirements = [x for x in requirements if onlyname(x) not in target_available_packages] # did we already installed the libs ? if exists(self.applibs_dir) and self.state.get("cache.applibs", "") == requirements: self.debug("Application requirements already installed, pass") return # recreate applibs self.rmdir(self.applibs_dir) self.mkdir(self.applibs_dir) # ok now check the availability of all requirements for requirement in requirements: self._install_application_requirement(requirement) # everything goes as expected, save this state! self.state["cache.applibs"] = requirements def _install_application_requirement(self, module): self._ensure_virtualenv() # resetup distribute, just in case self.debug("Install distribute") self.cmd( "curl http://python-distribute.org/distribute_setup.py | venv/bin/python", get_stdout=True, cwd=self.buildozer_dir, ) self.debug("Install requirement {} in virtualenv".format(module)) self.cmd( "pip-2.7 install --download-cache={} --target={} {}".format( self.global_cache_dir, self.applibs_dir, module ), env=self.env_venv, cwd=self.buildozer_dir, ) def _ensure_virtualenv(self): if hasattr(self, "venv"): return self.venv = join(self.buildozer_dir, "venv") if not self.file_exists(self.venv): self.cmd("virtualenv-2.7 --python=python2.7 ./venv", cwd=self.buildozer_dir) # read virtualenv output and parse it output = self.cmd('bash -c "source venv/bin/activate && env"', get_stdout=True, cwd=self.buildozer_dir) self.env_venv = copy(self.environ) for line in output[0].splitlines(): args = line.split("=", 1) if len(args) != 2: continue key, value = args if key in ("VIRTUAL_ENV", "PATH"): self.env_venv[key] = value if "PYTHONHOME" in self.env_venv: del self.env_venv["PYTHONHOME"] # ensure any sort of compilation will fail self.env_venv["CC"] = "/bin/false" self.env_venv["CXX"] = "/bin/false" def mkdir(self, dn): if exists(dn): return self.debug("Create directory {0}".format(dn)) makedirs(dn) def rmdir(self, dn): if not exists(dn): return self.debug("Remove directory and subdirectory {}".format(dn)) rmtree(dn) def file_exists(self, *args): return exists(join(*args)) def file_rename(self, source, target, cwd=None): if cwd: source = join(cwd, source) target = join(cwd, target) 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 :( # tf = tarfile.open(archive, 'r:*') # tf.extractall(path=cwd) # tf.close() self.cmd("tar xzf {0}".format(archive), cwd=cwd) return if archive.endswith(".tbz2") or archive.endswith(".tar.bz2"): # XXX same as before self.cmd("tar xjf {0}".format(archive), cwd=cwd) return if archive.endswith(".zip"): archive = join(cwd, archive) zf = zipfile.ZipFile(archive) zf.extractall(path=cwd) zf.close() return raise Exception("Unhandled extraction for type {0}".format(archive)) def file_copytree(self, src, dest): print "copy {} to {}".format(src, dest) if os.path.isdir(src): if not os.path.isdir(dest): os.makedirs(dest) files = os.listdir(src) for f in files: self.file_copytree(os.path.join(src, f), os.path.join(dest, f)) else: copyfile(src, dest) def clean_platform(self): self.info("Clean the platform build directory") if not exists(self.platform_dir): return rmtree(self.platform_dir) def download(self, url, filename, cwd=None): def report_hook(index, blksize, size): if size <= 0: progression = "{0} bytes".format(index * blksize) else: progression = "{0:.2f}%".format(index * blksize * 100.0 / float(size)) print "- Download", progression, "\r", stdout.flush() url = url + filename if cwd: filename = join(cwd, filename) if self.file_exists(filename): unlink(filename) self.debug("Downloading {0}".format(url)) urlretrieve(url, filename, report_hook) return filename def get_version(self): c = self.config has_version = c.has_option("app", "version") has_regex = c.has_option("app", "version.regex") has_filename = c.has_option("app", "version.filename") # version number specified if has_version: if has_regex or has_filename: raise Exception("version.regex and version.filename conflict with version") return c.get("app", "version") # search by regex if has_regex or has_filename: if has_regex and not has_filename: raise Exception("version.filename is missing") if has_filename and not has_regex: raise Exception("version.regex is missing") fn = c.get("app", "version.filename") with open(fn) as fd: data = fd.read() regex = c.get("app", "version.regex") match = search(regex, data) if not match: raise Exception("Unable to find capture version in {0}\n" " (looking for `{1}`)".format(fn, regex)) version = match.groups()[0] self.debug("Captured version: {0}".format(version)) return version raise Exception("Missing version or version.regex + version.filename") def build_application(self): self._copy_application_sources() self._copy_application_libs() self._add_sitecustomize() def _copy_application_sources(self): # XXX clean the inclusion/exclusion algo. 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", "") exclude_dirs = self.config.getlist("app", "source.exclude_dirs", "") exclude_patterns = self.config.getlist("app", "source.exclude_patterns", "") 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): # avoid hidden directory if True in [x.startswith(".") for x in root.split(sep)]: continue # need to have sort-of normalization. Let's say you want to exclude # image directory but not images, the filtered_root must have a / at # the end, same for the exclude_dir. And then we can safely compare filtered_root = root[len(source_dir) + 1 :].lower() if filtered_root: filtered_root += "/" # manual exclude_dirs approach is_excluded = False for exclude_dir in exclude_dirs: if exclude_dir[-1] != "/": exclude_dir += "/" if filtered_root.startswith(exclude_dir): is_excluded = True break if is_excluded: continue # pattern matching for pattern in exclude_patterns: if fnmatch(filtered_root, pattern): is_excluded = True break if is_excluded: continue for fn in files: # avoid hidden files if fn.startswith("."): continue # exclusion by pattern matching is_excluded = False dfn = fn.lower() if filtered_root: dfn = join(filtered_root, fn) for pattern in exclude_patterns: if fnmatch(dfn, pattern): is_excluded = True break if is_excluded: continue # filter based on the extension # TODO more filters basename, ext = splitext(fn) if ext: ext = ext[1:] if include_exts and ext not in include_exts: continue if exclude_exts and ext in exclude_exts: continue sfn = join(root, fn) rfn = realpath(join(app_dir, root[len(source_dir) + 1 :], fn)) # ensure the directory exists dfn = dirname(rfn) self.mkdir(dfn) # copy! self.debug("Copy {0}".format(sfn)) copyfile(sfn, rfn) def _copy_application_libs(self): # copy also the libs copytree(self.applibs_dir, join(self.app_dir, "_applibs")) def _add_sitecustomize(self): copyfile(join(dirname(__file__), "sitecustomize.py"), join(self.app_dir, "sitecustomize.py")) main_py = join(self.app_dir, "service", "main.py") if not self.file_exists(main_py): # self.error('Unable to patch main_py to add applibs directory.') return header = "import sys, os; " "sys.path = [os.path.join(os.getcwd()," '"..", "_applibs")] + sys.path\n' with open(main_py, "rb") as fd: data = fd.read() data = header + data with open(main_py, "wb") as fd: fd.write(data) self.info("Patched service/main.py to include applibs") def namify(self, name): """Return a "valid" name from a name with lot of invalid chars (allowed characters: a-z, A-Z, 0-9, -, _) """ return re.sub("[^a-zA-Z0-9_\-]", "_", name) @property def root_dir(self): return realpath(join(dirname(self.specfilename))) @property def buildozer_dir(self): return join(self.root_dir, ".buildozer") @property def bin_dir(self): return join(self.root_dir, "bin") @property def platform_dir(self): return join(self.buildozer_dir, self.targetname, "platform") @property def app_dir(self): return join(self.buildozer_dir, self.targetname, "app") @property def applibs_dir(self): return join(self.buildozer_dir, "applibs") @property def global_buildozer_dir(self): return join(expanduser("~"), ".buildozer") @property def global_platform_dir(self): return join(self.global_buildozer_dir, self.targetname, "platform") @property def global_packages_dir(self): return join(self.global_buildozer_dir, self.targetname, "packages") @property 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) # # command line invocation # def targets(self): for fn in listdir(join(dirname(__file__), "targets")): if fn.startswith(".") or fn.startswith("__"): continue if not fn.endswith(".py"): continue target = fn[:-3] try: m = __import__("buildozer.targets.{0}".format(target), fromlist=["buildozer"]) yield target, m except: raise pass def usage(self): print "Usage:" print " buildozer [--profile <name>] [--verbose] [target] <command>..." print " buildozer --version" print print "Available targets:" targets = list(self.targets()) for target, m in targets: doc = m.__doc__.strip().splitlines()[0].strip() print " {0:<18} {1}".format(target, doc) print print "Global commands (without target):" cmds = [x for x in dir(self) if x.startswith("cmd_")] for cmd in cmds: name = cmd[4:] meth = getattr(self, cmd) doc = [x for x in meth.__doc__.strip().splitlines()][0].strip() print " {0:<18} {1}".format(name, doc) print print "Target commands:" print " clean Clean the target environment" print " update Update the target dependencies" print " debug Build the application in debug mode" print " release Build the application in release mode" print " deploy Deploy the application on the device" print " run Run the application on the device" print " serve Serve the bin directory via SimpleHTTPServer" for target, m in targets: mt = m.get_target(self) commands = mt.get_custom_commands() if not commands: continue print print 'Target "{0}" commands:'.format(target) for command, doc in commands: doc = doc.strip().splitlines()[0].strip() print " {0:<18} {1}".format(command, doc) print def run_default(self): self.check_build_layout() if "buildozer:defaultcommand" not in self.state: print "No default command set." print 'Use "buildozer setdefault <command args...>"' print 'Use "buildozer help" for a list of all commands"' exit(1) cmd = self.state["buildozer:defaultcommand"] self.run_command(cmd) 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 elif arg in ("-h", "--help"): self.usage() exit(0) elif arg in ("-p", "--profile"): self.config_profile = args.pop(0) elif arg == "--version": print "Buildozer {0}".format(__version__) exit(0) self._merge_config_profile() if not args: self.run_default() return command, args = args[0], args[1:] cmd = "cmd_{0}".format(command) # internal commands ? if hasattr(self, cmd): getattr(self, cmd)(*args) return # maybe it's a target? targets = [x[0] for x in self.targets()] if command not in targets: print "Unknown command/target", command exit(1) self.set_target(command) self.target.run_commands(args) def cmd_init(self, *args): """Create a initial buildozer.spec in the current directory """ if exists("buildozer.spec"): print "ERROR: You already have a buildozer.spec file." exit(1) copyfile(join(dirname(__file__), "default.spec"), "buildozer.spec") print "File buildozer.spec created, ready to customize!" def cmd_clean(self, *args): """Clean the whole Buildozer environment. """ pass def cmd_help(self, *args): """Show the Buildozer help. """ self.usage() def cmd_setdefault(self, *args): """Set the default command to do when to arguments are given """ self.check_build_layout() self.state["buildozer:defaultcommand"] = args def cmd_version(self, *args): """Show the Buildozer version """ print "Buildozer {0}".format(__version__) def cmd_serve(self, *args): """Serve the bin directory via SimpleHTTPServer """ os.chdir(self.bin_dir) handler = SimpleHTTPServer.SimpleHTTPRequestHandler httpd = SocketServer.TCPServer(("", SIMPLE_HTTP_SERVER_PORT), handler) print ("Serving via HTTP at port {}".format(SIMPLE_HTTP_SERVER_PORT)) print ("Press Ctrl+c to quit serving.") httpd.serve_forever() # # Private # def _merge_config_profile(self): profile = self.config_profile if not profile: return for section in self.config.sections(): # extract the profile part from the section name # example: [app@default,hd] parts = section.split("@", 1) if len(parts) < 2: continue # create a list that contain all the profiles of the current section # ['default', 'hd'] section_base, section_profiles = parts section_profiles = section_profiles.split(",") if profile not in section_profiles: continue # the current profile is one available in the section # merge with the general section, or make it one. if not self.config.has_section(section_base): self.config.add_section(section_base) for name, value in self.config.items(section): print "merged ({}, {}) into {} (profile is {})".format(name, value, section_base, profile) self.config.set(section_base, name, value) def _get_config_list_values(self, *args, **kwargs): kwargs["with_values"] = True return self._get_config_list(*args, **kwargs) def _get_config_list(self, section, token, default=None, with_values=False): # monkey-patch method for ConfigParser # get a key as a list of string, seperated from the comma # check if an env var exists that should replace the file config set_config_token_from_env(section, token, self.config) # if a section:token is defined, let's use the content as a list. l_section = "{}:{}".format(section, token) if self.config.has_section(l_section): values = self.config.options(l_section) if with_values: return ["{}={}".format(key, self.config.get(l_section, key)) for key in values] else: return [x.strip() for x in values] values = self.config.getdefault(section, token, "") if not values: return default values = values.split(",") if not values: return default return [x.strip() for x in values] def _get_config_default(self, section, token, default=None): # monkey-patch method for ConfigParser # get an appropriate env var if it exists, else # get a key in a section, or the default # check if an env var exists that should replace the file config set_config_token_from_env(section, token, self.config) if not self.config.has_section(section): return default if not self.config.has_option(section, token): return default return self.config.get(section, token) def _get_config_bool(self, section, token, default=False): # monkey-patch method for ConfigParser # get a key in a section, or the default # check if an env var exists that should replace the file config set_config_token_from_env(section, token, self.config) if not self.config.has_section(section): return default if not self.config.has_option(section, token): return default return self.config.getboolean(section, token)
class Buildozer(object): standard_cmds = ('clean', 'update', 'debug', 'release', 'deploy', 'run') def __init__(self, filename='buildozer.spec', target=None): super(Buildozer, self).__init__() self.log_level = 1 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 if exists(filename): self.config.read(filename) self.check_configuration_tokens() try: self.log_level = int(self.config.getdefault( 'buildozer', 'log_level', '1')) except: pass self.targetname = None self.target = None if target: self.set_target(target) def set_target(self, target): '''Set the target to use (one of buildozer.targets, such as "android") ''' self.targetname = target m = __import__('buildozer.targets.{0}'.format(target), fromlist=['buildozer']) self.target = m.get_target(self) self.check_build_layout() self.check_configuration_tokens() self.target.check_configuration_tokens() def prepare_for_build(self): '''Prepare the build. ''' assert(self.target is not None) if hasattr(self.target, '_build_prepared'): return self.info('Preparing build') self.info('Check requirements for {0}'.format(self.targetname)) self.target.check_requirements() self.info('Install platform') self.target.install_platform() self.info('Check application requirements') self.check_application_requirements() self.info('Compile platform') self.target.compile_platform() # flag to prevent multiple build self.target._build_prepared = True def build(self): '''Do the build. The target can set build_mode to 'release' or 'debug' before calling this method. (:meth:`prepare_for_build` must have been call before.) ''' assert(self.target is not None) assert(hasattr(self.target, '_build_prepared')) if hasattr(self.target, '_build_done'): return # 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') self.target.build_package() # flag to prevent multiple build self.target._build_done = True # # Log functions # def log(self, level, msg): if level > self.log_level: return if USE_COLOR: color = COLOR_SEQ.format(30 + LOG_LEVELS_C[level]) print ''.join((RESET_SEQ, color, '# ', msg, RESET_SEQ)) else: print LOG_LEVELS_T[level], msg def debug(self, msg): self.log(2, msg) def info(self, msg): self.log(1, msg) def error(self, msg): self.log(0, msg) # # Internal check methods # def checkbin(self, msg, fn): self.debug('Search for {0}'.format(msg)) if exists(fn): return realpath(fn) for dn in environ['PATH'].split(':'): rfn = realpath(join(dn, fn)) if exists(rfn): self.debug(' -> found at {0}'.format(rfn)) return rfn raise Exception(msg + 'not found') def cmd(self, command, **kwargs): #print ' '.join(['{0}={1}'.format(*args) for args in # self.environ.iteritems()]) # prepare the environ, based on the system + our own env env = copy(environ) env.update(self.environ) # prepare the process kwargs.setdefault('env', env) kwargs.setdefault('stdout', PIPE) kwargs.setdefault('stderr', PIPE) kwargs.setdefault('close_fds', True) kwargs.setdefault('shell', True) kwargs.setdefault('show_output', self.log_level > 1) 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) 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 process = Popen(command, **kwargs) # prepare fds fd_stdout = process.stdout.fileno() fd_stderr = process.stderr.fileno() fcntl.fcntl( fd_stdout, fcntl.F_SETFL, fcntl.fcntl(fd_stdout, fcntl.F_GETFL) | os.O_NONBLOCK) fcntl.fcntl( fd_stderr, fcntl.F_SETFL, fcntl.fcntl(fd_stderr, fcntl.F_GETFL) | os.O_NONBLOCK) ret_stdout = [] if get_stdout else None ret_stderr = [] if get_stderr else None while True: readx = select([fd_stdout, fd_stderr], [], [])[0] if fd_stdout in readx: chunk = process.stdout.read() if chunk == '': break if get_stdout: ret_stdout.append(chunk) if show_output: stdout.write(chunk) if fd_stderr in readx: chunk = process.stderr.read() if chunk == '': break if get_stderr: ret_stderr.append(chunk) if show_output: stderr.write(chunk) stdout.flush() stderr.flush() process.communicate() 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, process.returncode) def check_configuration_tokens(self): '''Ensure the spec file is 'correct'. ''' self.info('Check configuration tokens') get = self.config.getdefault errors = [] adderror = errors.append if not get('app', 'title', ''): adderror('[app] "title" is missing') if not get('app', 'package.name', ''): adderror('[app] "package.name" is missing') if not get('app', 'source.dir', ''): adderror('[app] "source.dir" is missing') version = get('app', 'version', '') version_regex = get('app', 'version.regex', '') if not version and not version_regex: adderror('[app] One of "version" or "version.regex" must be set') if version and version_regex: adderror('[app] Conflict between "version" and "version.regex"' ', only one can be used.') if version_regex and not get('app', 'version.filename', ''): adderror('[app] "version.filename" is missing' ', required by "version.regex"') if errors: self.error('{0} error(s) found in the buildozer.spec'.format( len(errors))) for error in errors: print error exit(1) def check_build_layout(self): '''Ensure the build (local and global) directory layout and files are ready. ''' self.info('Ensure build layout') if not exists(self.specfilename): print 'No {0} found in the current directory. Abandon.'.format( self.specfilename) exit(1) # create global dir self.mkdir(self.global_buildozer_dir) self.mkdir(self.global_cache_dir) # create local dir specdir = dirname(self.specfilename) self.mkdir(join(specdir, '.buildozer')) self.mkdir(join(specdir, 'bin')) self.mkdir(self.applibs_dir) self.state = shelve.open(join(self.buildozer_dir, 'state.db')) if self.targetname: target = self.targetname self.mkdir(join(self.global_platform_dir, target, 'platform')) self.mkdir(join(specdir, '.buildozer', target, 'platform')) self.mkdir(join(specdir, '.buildozer', target, 'app')) def check_application_requirements(self): '''Ensure the application requirements are all available and ready to be packaged as well. ''' requirements = self.config.getlist('app', 'requirements', '') target_available_packages = self.target.get_available_packages() # remove all the requirements that the target can compile requirements = [x for x in requirements if x not in target_available_packages] # did we already installed the libs ? if exists(self.applibs_dir) and \ self.state.get('cache.applibs', '') == requirements: self.debug('Application requirements already installed, pass') return # recreate applibs self.rmdir(self.applibs_dir) self.mkdir(self.applibs_dir) # ok now check the availability of all requirements for requirement in requirements: self._install_application_requirement(requirement) # everything goes as expected, save this state! self.state['cache.applibs'] = requirements def _install_application_requirement(self, module): self._ensure_virtualenv() self.debug('Install requirement {} in virtualenv'.format(module)) self.cmd('pip-2.7 install --download-cache={} --target={} {}'.format( self.global_cache_dir, self.applibs_dir, module), env=self.env_venv, cwd=self.buildozer_dir) def _ensure_virtualenv(self): if hasattr(self, 'venv'): return self.venv = join(self.buildozer_dir, 'venv') if not self.file_exists(self.venv): self.cmd('virtualenv-2.7 --python=python2.7 ./venv', cwd=self.buildozer_dir) # read virtualenv output and parse it output = self.cmd('bash -c "source venv/bin/activate && env"', get_stdout=True, cwd=self.buildozer_dir) self.env_venv = copy(self.environ) for line in output[0].splitlines(): args = line.split('=', 1) if len(args) != 2: continue key, value = args if key in ('VIRTUAL_ENV', 'PATH'): self.env_venv[key] = value if 'PYTHONHOME' in self.env_venv: del self.env_venv['PYTHONHOME'] # ensure any sort of compilation will fail self.env_venv['CC'] = '/bin/false' self.env_venv['CXX'] = '/bin/false' def mkdir(self, dn): if exists(dn): return self.debug('Create directory {0}'.format(dn)) makedirs(dn) def rmdir(self, dn): if not exists(dn): return self.debug('Remove directory and subdirectory {}'.format(dn)) rmtree(dn) def file_exists(self, *args): return exists(join(*args)) def file_rename(self, source, target, cwd=None): if cwd: source = join(cwd, source) target = join(cwd, target) 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 :( #tf = tarfile.open(archive, 'r:*') #tf.extractall(path=cwd) #tf.close() self.cmd('tar xzf {0}'.format(archive), cwd=cwd) return if archive.endswith('.tbz2') or archive.endswith('.tar.bz2'): # XXX same as before self.cmd('tar xjf {0}'.format(archive), cwd=cwd) return if archive.endswith('.zip'): zf = zipfile.ZipFile(archive) zf.extractall(path=cwd) zf.close() return raise Exception('Unhandled extraction for type {0}'.format(archive)) def clean_platform(self): self.info('Clean the platform build directory') if not exists(self.platform_dir): return rmtree(self.platform_dir) def download(self, url, filename, cwd=None): def report_hook(index, blksize, size): if size <= 0: progression = '{0} bytes'.format(index * blksize) else: progression = '{0:.2f}%'.format( index * blksize * 100. / float(size)) print '- Download', progression, '\r', stdout.flush() url = url + filename if cwd: filename = join(cwd, filename) if self.file_exists(filename): unlink(filename) self.debug('Downloading {0}'.format(url)) urlretrieve(url, filename, report_hook) return filename def get_version(self): c = self.config has_version = c.has_option('app', 'version') has_regex = c.has_option('app', 'version.regex') has_filename = c.has_option('app', 'version.filename') # version number specified if has_version: if has_regex or has_filename: raise Exception( 'version.regex and version.filename conflict with version') return c.get('app', 'version') # search by regex if has_regex or has_filename: if has_regex and not has_filename: raise Exception('version.filename is missing') if has_filename and not has_regex: raise Exception('version.regex is missing') fn = c.get('app', 'version.filename') with open(fn) as fd: data = fd.read() regex = c.get('app', 'version.regex') match = search(regex, data) if not match: raise Exception( 'Unable to found capture version in {0}'.format(fn)) version = match.groups()[0] self.debug('Captured version: {0}'.format(version)) return version 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): # avoid hidden directory if True in [x.startswith('.') for x in root.split(sep)]: continue for fn in files: # avoid hidden files if fn.startswith('.'): continue # filter based on the extension # TODO more filters basename, ext = splitext(fn) if ext: ext = ext[1:] if include_exts and ext not in include_exts: continue if exclude_exts and ext in exclude_exts: continue sfn = join(root, fn) rfn = realpath(join(app_dir, root[len(source_dir) + 1:], fn)) # ensure the directory exists dfn = dirname(rfn) self.mkdir(dfn) # copy! self.debug('Copy {0}'.format(sfn)) copyfile(sfn, rfn) def _copy_application_libs(self): # copy also the libs copytree(self.applibs_dir, join(self.app_dir, '_applibs')) def _patch_application_sources(self): # patch the 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 header = ('import sys, os; ' 'sys.path = [os.path.join(os.path.dirname(__file__),' '"_applibs")] + sys.path\n') with open(main_py, 'rb') as fd: data = fd.read() data = header + data with open(main_py, 'wb') as fd: fd.write(data) self.info('Patched main.py to include applibs') def namify(self, name): '''Return a "valid" name from a name with lot of invalid chars (allowed characters: a-z, A-Z, 0-9, -, _) ''' return re.sub('[^a-zA-Z0-9_\-]', '_', name) @property def buildozer_dir(self): return realpath(join( dirname(self.specfilename), '.buildozer')) @property def platform_dir(self): return join(self.buildozer_dir, self.targetname, 'platform') @property def app_dir(self): return join(self.buildozer_dir, self.targetname, 'app') @property def bin_dir(self): return realpath(join( dirname(self.specfilename), 'bin')) @property def applibs_dir(self): return join(self.buildozer_dir, 'applibs') @property def global_buildozer_dir(self): return join(expanduser('~'), '.buildozer') @property def global_platform_dir(self): return join(self.global_buildozer_dir, self.targetname, 'platform') @property 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) # # command line invocation # def targets(self): for fn in listdir(join(dirname(__file__), 'targets')): if fn.startswith('.') or fn.startswith('__'): continue if not fn.endswith('.py'): continue target = fn[:-3] try: m = __import__('buildozer.targets.{0}'.format(target), fromlist=['buildozer']) yield target, m except: raise pass def usage(self): print 'Usage: buildozer [--verbose] [target] [command1] [command2]' print print 'Available targets:' targets = list(self.targets()) for target, m in targets: doc = m.__doc__.strip().splitlines()[0].strip() print ' {0:<18} {1}'.format(target, doc) print print 'Global commands (without target):' cmds = [x for x in dir(self) if x.startswith('cmd_')] for cmd in cmds: name = cmd[4:] meth = getattr(self, cmd) doc = [x for x in meth.__doc__.strip().splitlines()][0].strip() print ' {0:<18} {1}'.format(name, doc) print print 'Target commands:' print ' clean Clean the target environment' print ' update Update the target dependencies' print ' debug Build the application in debug mode' print ' release Build the application in release mode' print ' deploy Deploy the application on the device' print ' run Run the application on the device' for target, m in targets: mt = m.get_target(self) commands = mt.get_custom_commands() if not commands: continue print print 'Target "{0}" commands:'.format(target) for command, doc in commands: doc = doc.strip().splitlines()[0].strip() print ' {0:<18} {1}'.format(command, doc) print def run_default(self): self.check_build_layout() if 'buildozer:defaultcommand' not in self.state: print 'No default command set.' print 'Use "buildozer setdefault <command args...>"' print 'Use "buildozer help" for a list of all commands"' exit(1) cmd = self.state['buildozer:defaultcommand'] self.run_command(cmd) 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 {0}'.format(__version__) exit(0) if not args: self.run_default() return command, args = args[0], args[1:] cmd = 'cmd_{0}'.format(command) # internal commands ? if hasattr(self, cmd): getattr(self, cmd)(*args) return # maybe it's a target? targets = [x[0] for x in self.targets()] if command not in targets: print 'Unknow command/target', command exit(1) self.set_target(command) self.target.run_commands(args) def cmd_init(self, *args): '''Create a initial buildozer.spec in the current directory ''' if exists('buildozer.spec'): print 'ERROR: You already have a buildozer.spec file.' exit(1) copyfile(join(dirname(__file__), 'default.spec'), 'buildozer.spec') print 'File buildozer.spec created, ready to customize!' def cmd_clean(self, *args): '''Clean the whole Buildozer environment. ''' pass def cmd_help(self, *args): '''Show the Buildozer help. ''' self.usage() def cmd_setdefault(self, *args): '''Set the default command to do when to arguments are given ''' self.check_build_layout() self.state['buildozer:defaultcommand'] = args def cmd_version(self, *args): '''Show the Buildozer version ''' print 'Buildozer {0}'.format(__version__) # # Private # def _get_config_list(self, section, token, default=None): # monkey-patch method for ConfigParser # get a key as a list of string, seperated from the comma values = self.config.getdefault(section, token, default).split(',') return [x.strip() for x in values] def _get_config_default(self, section, token, default=None): # monkey-patch method for ConfigParser # get a key in a section, or the default if not self.config.has_section(section): return default if not self.config.has_option(section, token): return default return self.config.get(section, token)