1 # -*- coding: utf-8 -*-
3 # common.py - part of the FDroid server tools
4 # Copyright (C) 2010-13, Ciaran Gultnieks, ciaran@ciarang.com
5 # Copyright (C) 2013-2014 Daniel Martà <mvdan@mvdan.cc>
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
33 from distutils.version import LooseVersion
42 def get_default_config():
44 'sdk_path': os.getenv("ANDROID_HOME") or "",
45 'ndk_path': os.getenv("ANDROID_NDK") or "",
46 'build_tools': "20.0.0",
50 'sync_from_local_copy_dir': False,
51 'update_stats': False,
52 'stats_to_carbon': False,
54 'build_server_always': False,
55 'keystore': os.path.join(os.getenv("HOME"), '.local', 'share', 'fdroidserver', 'keystore.jks'),
56 'smartcardoptions': [],
62 'repo_url': "https://MyFirstFDroidRepo.org/fdroid/repo",
63 'repo_name': "My First FDroid Repo Demo",
64 'repo_icon': "fdroid-icon.png",
65 'repo_description': '''
66 This is a repository of apps to be used with FDroid. Applications in this
67 repository are either official binaries built by the original application
68 developers, or are binaries built from source by the admin of f-droid.org
69 using the tools on https://gitlab.com/u/fdroid.
75 def read_config(opts, config_file='config.py'):
76 """Read the repository config
78 The config is read from config_file, which is in the current directory when
79 any of the repo management commands are used.
81 global config, options, env
83 if config is not None:
85 if not os.path.isfile(config_file):
86 logging.critical("Missing config file - is this a repo directory?")
93 logging.debug("Reading %s" % config_file)
94 execfile(config_file, config)
96 # smartcardoptions must be a list since its command line args for Popen
97 if 'smartcardoptions' in config:
98 config['smartcardoptions'] = config['smartcardoptions'].split(' ')
99 elif 'keystore' in config and config['keystore'] == 'NONE':
100 # keystore='NONE' means use smartcard, these are required defaults
101 config['smartcardoptions'] = ['-storetype', 'PKCS11', '-providerName',
102 'SunPKCS11-OpenSC', '-providerClass',
103 'sun.security.pkcs11.SunPKCS11',
104 '-providerArg', 'opensc-fdroid.cfg']
106 if any(k in config for k in ["keystore", "keystorepass", "keypass"]):
107 st = os.stat(config_file)
108 if st.st_mode & stat.S_IRWXG or st.st_mode & stat.S_IRWXO:
109 logging.warn("unsafe permissions on {0} (should be 0600)!".format(config_file))
111 defconfig = get_default_config()
112 for k, v in defconfig.items():
116 # Expand environment variables
117 for k, v in config.items():
120 v = os.path.expanduser(v)
121 config[k] = os.path.expandvars(v)
123 if not test_sdk_exists(config):
126 if not test_build_tools_exists(config):
131 os.path.join(config['sdk_path'], 'build-tools', config['build_tools'], 'aapt'),
134 os.path.join(config['sdk_path'], 'tools', 'zipalign'),
135 os.path.join(config['sdk_path'], 'build-tools', config['build_tools'], 'zipalign'),
138 os.path.join(config['sdk_path'], 'tools', 'android'),
141 os.path.join(config['sdk_path'], 'platform-tools', 'adb'),
145 for b, paths in bin_paths.items():
148 if os.path.isfile(path):
151 if config[b] is None:
152 logging.warn("Could not find %s in any of the following paths:\n%s" % (
153 b, '\n'.join(paths)))
155 # There is no standard, so just set up the most common environment
158 for n in ['ANDROID_HOME', 'ANDROID_SDK']:
159 env[n] = config['sdk_path']
160 for n in ['ANDROID_NDK', 'NDK']:
161 env[n] = config['ndk_path']
163 for k in ["keystorepass", "keypass"]:
165 write_password_file(k)
167 for k in ["repo_description", "archive_description"]:
169 config[k] = clean_description(config[k])
171 if 'serverwebroot' in config:
172 if isinstance(config['serverwebroot'], basestring):
173 roots = [config['serverwebroot']]
174 elif all(isinstance(item, basestring) for item in config['serverwebroot']):
175 roots = config['serverwebroot']
177 raise TypeError('only accepts strings, lists, and tuples')
179 for rootstr in roots:
180 # since this is used with rsync, where trailing slashes have
181 # meaning, ensure there is always a trailing slash
182 if rootstr[-1] != '/':
184 rootlist.append(rootstr.replace('//', '/'))
185 config['serverwebroot'] = rootlist
190 def test_sdk_exists(c):
191 if c['sdk_path'] is None:
192 # c['sdk_path'] is set to the value of ANDROID_HOME by default
193 logging.error('No Android SDK found! ANDROID_HOME is not set and sdk_path is not in config.py!')
194 logging.error('You can use ANDROID_HOME to set the path to your SDK, i.e.:')
195 logging.error('\texport ANDROID_HOME=/opt/android-sdk')
197 if not os.path.exists(c['sdk_path']):
198 logging.critical('Android SDK path "' + c['sdk_path'] + '" does not exist!')
200 if not os.path.isdir(c['sdk_path']):
201 logging.critical('Android SDK path "' + c['sdk_path'] + '" is not a directory!')
203 for d in ['build-tools', 'platform-tools', 'tools']:
204 if not os.path.isdir(os.path.join(c['sdk_path'], d)):
205 logging.critical('Android SDK path "%s" does not contain "%s/"!' % (
211 def test_build_tools_exists(c):
212 if not test_sdk_exists(c):
214 build_tools = os.path.join(c['sdk_path'], 'build-tools')
215 versioned_build_tools = os.path.join(build_tools, c['build_tools'])
216 if not os.path.isdir(versioned_build_tools):
217 logging.critical('Android Build Tools path "'
218 + versioned_build_tools + '" does not exist!')
223 def write_password_file(pwtype, password=None):
225 writes out passwords to a protected file instead of passing passwords as
226 command line argments
228 filename = '.fdroid.' + pwtype + '.txt'
229 fd = os.open(filename, os.O_CREAT | os.O_TRUNC | os.O_WRONLY, 0600)
231 os.write(fd, config[pwtype])
233 os.write(fd, password)
235 config[pwtype + 'file'] = filename
238 # Given the arguments in the form of multiple appid:[vc] strings, this returns
239 # a dictionary with the set of vercodes specified for each package.
240 def read_pkg_args(args, allow_vercodes=False):
247 if allow_vercodes and ':' in p:
248 package, vercode = p.split(':')
250 package, vercode = p, None
251 if package not in vercodes:
252 vercodes[package] = [vercode] if vercode else []
254 elif vercode and vercode not in vercodes[package]:
255 vercodes[package] += [vercode] if vercode else []
260 # On top of what read_pkg_args does, this returns the whole app metadata, but
261 # limiting the builds list to the builds matching the vercodes specified.
262 def read_app_args(args, allapps, allow_vercodes=False):
264 vercodes = read_pkg_args(args, allow_vercodes)
270 for appid, app in allapps.iteritems():
271 if appid in vercodes:
274 if len(apps) != len(vercodes):
275 allids = [app["id"] for app in allapps]
278 logging.critical("No such package: %s" % p)
279 raise FDroidException("Found invalid app ids in arguments")
281 raise FDroidException("No packages specified")
284 for appid, app in apps.iteritems():
288 app['builds'] = [b for b in app['builds'] if b['vercode'] in vc]
289 if len(app['builds']) != len(vercodes[appid]):
291 allvcs = [b['vercode'] for b in app['builds']]
292 for v in vercodes[appid]:
294 logging.critical("No such vercode %s for app %s" % (v, appid))
297 raise FDroidException("Found invalid vercodes for some apps")
302 def has_extension(filename, extension):
303 name, ext = os.path.splitext(filename)
304 ext = ext.lower()[1:]
305 return ext == extension
310 def clean_description(description):
311 'Remove unneeded newlines and spaces from a block of description text'
313 # this is split up by paragraph to make removing the newlines easier
314 for paragraph in re.split(r'\n\n', description):
315 paragraph = re.sub('\r', '', paragraph)
316 paragraph = re.sub('\n', ' ', paragraph)
317 paragraph = re.sub(' {2,}', ' ', paragraph)
318 paragraph = re.sub('^\s*(\w)', r'\1', paragraph)
319 returnstring += paragraph + '\n\n'
320 return returnstring.rstrip('\n')
323 def apknameinfo(filename):
325 filename = os.path.basename(filename)
326 if apk_regex is None:
327 apk_regex = re.compile(r"^(.+)_([0-9]+)\.apk$")
328 m = apk_regex.match(filename)
330 result = (m.group(1), m.group(2))
331 except AttributeError:
332 raise FDroidException("Invalid apk name: %s" % filename)
336 def getapkname(app, build):
337 return "%s_%s.apk" % (app['id'], build['vercode'])
340 def getsrcname(app, build):
341 return "%s_%s_src.tar.gz" % (app['id'], build['vercode'])
348 return app['Auto Name']
353 return '%s (%s)' % (app['Current Version'], app['Current Version Code'])
356 def getvcs(vcstype, remote, local):
358 return vcs_git(remote, local)
359 if vcstype == 'git-svn':
360 return vcs_gitsvn(remote, local)
362 return vcs_hg(remote, local)
364 return vcs_bzr(remote, local)
365 if vcstype == 'srclib':
366 if local != os.path.join('build', 'srclib', remote):
367 raise VCSException("Error: srclib paths are hard-coded!")
368 return getsrclib(remote, os.path.join('build', 'srclib'), raw=True)
370 raise VCSException("Deprecated vcs type 'svn' - please use 'git-svn' instead")
371 raise VCSException("Invalid vcs type " + vcstype)
374 def getsrclibvcs(name):
375 if name not in metadata.srclibs:
376 raise VCSException("Missing srclib " + name)
377 return metadata.srclibs[name]['Repo Type']
381 def __init__(self, remote, local):
383 # svn, git-svn and bzr may require auth
385 if self.repotype() in ('git-svn', 'bzr'):
387 self.username, remote = remote.split('@')
388 if ':' not in self.username:
389 raise VCSException("Password required with username")
390 self.username, self.password = self.username.split(':')
394 self.clone_failed = False
395 self.refreshed = False
401 # Take the local repository to a clean version of the given revision, which
402 # is specificed in the VCS's native format. Beforehand, the repository can
403 # be dirty, or even non-existent. If the repository does already exist
404 # locally, it will be updated from the origin, but only once in the
405 # lifetime of the vcs object.
406 # None is acceptable for 'rev' if you know you are cloning a clean copy of
407 # the repo - otherwise it must specify a valid revision.
408 def gotorevision(self, rev):
410 if self.clone_failed:
411 raise VCSException("Downloading the repository already failed once, not trying again.")
413 # The .fdroidvcs-id file for a repo tells us what VCS type
414 # and remote that directory was created from, allowing us to drop it
415 # automatically if either of those things changes.
416 fdpath = os.path.join(self.local, '..',
417 '.fdroidvcs-' + os.path.basename(self.local))
418 cdata = self.repotype() + ' ' + self.remote
421 if os.path.exists(self.local):
422 if os.path.exists(fdpath):
423 with open(fdpath, 'r') as f:
424 fsdata = f.read().strip()
430 "Repository details for %s changed - deleting" % (
434 logging.info("Repository details for %s missing - deleting" % (
437 shutil.rmtree(self.local)
442 self.gotorevisionx(rev)
443 except FDroidException, e:
446 # If necessary, write the .fdroidvcs file.
447 if writeback and not self.clone_failed:
448 with open(fdpath, 'w') as f:
454 # Derived classes need to implement this. It's called once basic checking
455 # has been performend.
456 def gotorevisionx(self, rev):
457 raise VCSException("This VCS type doesn't define gotorevisionx")
459 # Initialise and update submodules
460 def initsubmodules(self):
461 raise VCSException('Submodules not supported for this vcs type')
463 # Get a list of all known tags
465 raise VCSException('gettags not supported for this vcs type')
467 # Get a list of latest number tags
468 def latesttags(self, number):
469 raise VCSException('latesttags not supported for this vcs type')
471 # Get current commit reference (hash, revision, etc)
473 raise VCSException('getref not supported for this vcs type')
475 # Returns the srclib (name, path) used in setting up the current
486 # If the local directory exists, but is somehow not a git repository, git
487 # will traverse up the directory tree until it finds one that is (i.e.
488 # fdroidserver) and then we'll proceed to destroy it! This is called as
491 p = SilentPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local)
492 result = p.output.rstrip()
493 if not result.endswith(self.local):
494 raise VCSException('Repository mismatch')
496 def gotorevisionx(self, rev):
497 if not os.path.exists(self.local):
499 p = FDroidPopen(['git', 'clone', self.remote, self.local])
500 if p.returncode != 0:
501 self.clone_failed = True
502 raise VCSException("Git clone failed", p.output)
506 # Discard any working tree changes
507 p = SilentPopen(['git', 'reset', '--hard'], cwd=self.local)
508 if p.returncode != 0:
509 raise VCSException("Git reset failed", p.output)
510 # Remove untracked files now, in case they're tracked in the target
511 # revision (it happens!)
512 p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
513 if p.returncode != 0:
514 raise VCSException("Git clean failed", p.output)
515 if not self.refreshed:
516 # Get latest commits and tags from remote
517 p = FDroidPopen(['git', 'fetch', 'origin'], cwd=self.local)
518 if p.returncode != 0:
519 raise VCSException("Git fetch failed", p.output)
520 p = SilentPopen(['git', 'fetch', '--prune', '--tags', 'origin'], cwd=self.local)
521 if p.returncode != 0:
522 raise VCSException("Git fetch failed", p.output)
523 # Recreate origin/HEAD as git clone would do it, in case it disappeared
524 p = SilentPopen(['git', 'remote', 'set-head', 'origin', '--auto'], cwd=self.local)
525 if p.returncode != 0:
526 lines = p.output.splitlines()
527 if 'Multiple remote HEAD branches' not in lines[0]:
528 raise VCSException("Git remote set-head failed", p.output)
529 branch = lines[1].split(' ')[-1]
530 p2 = SilentPopen(['git', 'remote', 'set-head', 'origin', branch], cwd=self.local)
531 if p2.returncode != 0:
532 raise VCSException("Git remote set-head failed", p.output + '\n' + p2.output)
533 self.refreshed = True
534 # origin/HEAD is the HEAD of the remote, e.g. the "default branch" on
535 # a github repo. Most of the time this is the same as origin/master.
536 rev = rev or 'origin/HEAD'
537 p = SilentPopen(['git', 'checkout', '-f', rev], cwd=self.local)
538 if p.returncode != 0:
539 raise VCSException("Git checkout of '%s' failed" % rev, p.output)
540 # Get rid of any uncontrolled files left behind
541 p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
542 if p.returncode != 0:
543 raise VCSException("Git clean failed", p.output)
545 def initsubmodules(self):
547 submfile = os.path.join(self.local, '.gitmodules')
548 if not os.path.isfile(submfile):
549 raise VCSException("No git submodules available")
551 # fix submodules not accessible without an account and public key auth
552 with open(submfile, 'r') as f:
553 lines = f.readlines()
554 with open(submfile, 'w') as f:
556 if 'git@github.com' in line:
557 line = line.replace('git@github.com:', 'https://github.com/')
561 ['git', 'reset', '--hard'],
562 ['git', 'clean', '-dffx'],
564 p = SilentPopen(['git', 'submodule', 'foreach', '--recursive'] + cmd, cwd=self.local)
565 if p.returncode != 0:
566 raise VCSException("Git submodule reset failed", p.output)
567 p = SilentPopen(['git', 'submodule', 'sync'], cwd=self.local)
568 if p.returncode != 0:
569 raise VCSException("Git submodule sync failed", p.output)
570 p = FDroidPopen(['git', 'submodule', 'update', '--init', '--force', '--recursive'], cwd=self.local)
571 if p.returncode != 0:
572 raise VCSException("Git submodule update failed", p.output)
576 p = SilentPopen(['git', 'tag'], cwd=self.local)
577 return p.output.splitlines()
579 def latesttags(self, alltags, number):
581 p = SilentPopen(['echo "' + '\n'.join(alltags) + '" | '
582 + 'xargs -I@ git log --format=format:"%at @%n" -1 @ | '
583 + 'sort -n | awk \'{print $2}\''],
584 cwd=self.local, shell=True)
585 return p.output.splitlines()[-number:]
588 class vcs_gitsvn(vcs):
593 # Damn git-svn tries to use a graphical password prompt, so we have to
594 # trick it into taking the password from stdin
596 if self.username is None:
598 return ('echo "%s" | DISPLAY="" ' % self.password, ' --username "%s"' % self.username)
600 # If the local directory exists, but is somehow not a git repository, git
601 # will traverse up the directory tree until it finds one that is (i.e.
602 # fdroidserver) and then we'll proceed to destory it! This is called as
605 p = SilentPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local)
606 result = p.output.rstrip()
607 if not result.endswith(self.local):
608 raise VCSException('Repository mismatch')
610 def gotorevisionx(self, rev):
611 if not os.path.exists(self.local):
613 gitsvn_cmd = '%sgit svn clone%s' % self.userargs()
614 if ';' in self.remote:
615 remote_split = self.remote.split(';')
616 for i in remote_split[1:]:
617 if i.startswith('trunk='):
618 gitsvn_cmd += ' -T %s' % i[6:]
619 elif i.startswith('tags='):
620 gitsvn_cmd += ' -t %s' % i[5:]
621 elif i.startswith('branches='):
622 gitsvn_cmd += ' -b %s' % i[9:]
623 p = SilentPopen([gitsvn_cmd + " %s %s" % (remote_split[0], self.local)], shell=True)
624 if p.returncode != 0:
625 self.clone_failed = True
626 raise VCSException("Git svn clone failed", p.output)
628 p = SilentPopen([gitsvn_cmd + " %s %s" % (self.remote, self.local)], shell=True)
629 if p.returncode != 0:
630 self.clone_failed = True
631 raise VCSException("Git svn clone failed", p.output)
635 # Discard any working tree changes
636 p = SilentPopen(['git', 'reset', '--hard'], cwd=self.local)
637 if p.returncode != 0:
638 raise VCSException("Git reset failed", p.output)
639 # Remove untracked files now, in case they're tracked in the target
640 # revision (it happens!)
641 p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
642 if p.returncode != 0:
643 raise VCSException("Git clean failed", p.output)
644 if not self.refreshed:
645 # Get new commits, branches and tags from repo
646 p = SilentPopen(['%sgit svn fetch %s' % self.userargs()], cwd=self.local, shell=True)
647 if p.returncode != 0:
648 raise VCSException("Git svn fetch failed")
649 p = SilentPopen(['%sgit svn rebase %s' % self.userargs()], cwd=self.local, shell=True)
650 if p.returncode != 0:
651 raise VCSException("Git svn rebase failed", p.output)
652 self.refreshed = True
654 rev = rev or 'master'
656 nospaces_rev = rev.replace(' ', '%20')
657 # Try finding a svn tag
658 for treeish in ['origin/', '']:
659 p = SilentPopen(['git', 'checkout', treeish + 'tags/' + nospaces_rev],
661 if p.returncode == 0:
663 if p.returncode != 0:
664 # No tag found, normal svn rev translation
665 # Translate svn rev into git format
666 rev_split = rev.split('/')
669 for treeish in ['origin/', '']:
670 if len(rev_split) > 1:
671 treeish += rev_split[0]
672 svn_rev = rev_split[1]
675 # if no branch is specified, then assume trunk (i.e. 'master' branch):
679 svn_rev = svn_rev if svn_rev[0] == 'r' else 'r' + svn_rev
681 p = SilentPopen(['git', 'svn', 'find-rev', '--before', svn_rev, treeish],
683 git_rev = p.output.rstrip()
685 if p.returncode == 0 and git_rev:
688 if p.returncode != 0 or not git_rev:
689 # Try a plain git checkout as a last resort
690 p = SilentPopen(['git', 'checkout', rev], cwd=self.local)
691 if p.returncode != 0:
692 raise VCSException("No git treeish found and direct git checkout of '%s' failed" % rev, p.output)
694 # Check out the git rev equivalent to the svn rev
695 p = SilentPopen(['git', 'checkout', git_rev], cwd=self.local)
696 if p.returncode != 0:
697 raise VCSException("Git checkout of '%s' failed" % rev, p.output)
699 # Get rid of any uncontrolled files left behind
700 p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
701 if p.returncode != 0:
702 raise VCSException("Git clean failed", p.output)
706 for treeish in ['origin/', '']:
707 d = os.path.join(self.local, '.git', 'svn', 'refs', 'remotes', treeish, 'tags')
713 p = SilentPopen(['git', 'svn', 'find-rev', 'HEAD'], cwd=self.local)
714 if p.returncode != 0:
716 return p.output.strip()
724 def gotorevisionx(self, rev):
725 if not os.path.exists(self.local):
726 p = SilentPopen(['hg', 'clone', self.remote, self.local])
727 if p.returncode != 0:
728 self.clone_failed = True
729 raise VCSException("Hg clone failed", p.output)
731 p = SilentPopen(['hg status -uS | xargs rm -rf'], cwd=self.local, shell=True)
732 if p.returncode != 0:
733 raise VCSException("Hg clean failed", p.output)
734 if not self.refreshed:
735 p = SilentPopen(['hg', 'pull'], cwd=self.local)
736 if p.returncode != 0:
737 raise VCSException("Hg pull failed", p.output)
738 self.refreshed = True
740 rev = rev or 'default'
743 p = SilentPopen(['hg', 'update', '-C', rev], cwd=self.local)
744 if p.returncode != 0:
745 raise VCSException("Hg checkout of '%s' failed" % rev, p.output)
746 p = SilentPopen(['hg', 'purge', '--all'], cwd=self.local)
747 # Also delete untracked files, we have to enable purge extension for that:
748 if "'purge' is provided by the following extension" in p.output:
749 with open(os.path.join(self.local, '.hg', 'hgrc'), "a") as myfile:
750 myfile.write("\n[extensions]\nhgext.purge=\n")
751 p = SilentPopen(['hg', 'purge', '--all'], cwd=self.local)
752 if p.returncode != 0:
753 raise VCSException("HG purge failed", p.output)
754 elif p.returncode != 0:
755 raise VCSException("HG purge failed", p.output)
758 p = SilentPopen(['hg', 'tags', '-q'], cwd=self.local)
759 return p.output.splitlines()[1:]
767 def gotorevisionx(self, rev):
768 if not os.path.exists(self.local):
769 p = SilentPopen(['bzr', 'branch', self.remote, self.local])
770 if p.returncode != 0:
771 self.clone_failed = True
772 raise VCSException("Bzr branch failed", p.output)
774 p = SilentPopen(['bzr', 'clean-tree', '--force', '--unknown', '--ignored'], cwd=self.local)
775 if p.returncode != 0:
776 raise VCSException("Bzr revert failed", p.output)
777 if not self.refreshed:
778 p = SilentPopen(['bzr', 'pull'], cwd=self.local)
779 if p.returncode != 0:
780 raise VCSException("Bzr update failed", p.output)
781 self.refreshed = True
783 revargs = list(['-r', rev] if rev else [])
784 p = SilentPopen(['bzr', 'revert'] + revargs, cwd=self.local)
785 if p.returncode != 0:
786 raise VCSException("Bzr revert of '%s' failed" % rev, p.output)
789 p = SilentPopen(['bzr', 'tags'], cwd=self.local)
790 return [tag.split(' ')[0].strip() for tag in
791 p.output.splitlines()]
794 def retrieve_string(app_dir, string, xmlfiles=None):
797 os.path.join(app_dir, 'res'),
798 os.path.join(app_dir, 'src', 'main'),
803 for res_dir in res_dirs:
804 for r, d, f in os.walk(res_dir):
805 if os.path.basename(r) == 'values':
806 xmlfiles += [os.path.join(r, x) for x in f if x.endswith('.xml')]
809 if string.startswith('@string/'):
810 string_search = re.compile(r'.*name="' + string[8:] + '".*?>([^<]+?)<.*').search
811 elif string.startswith('&') and string.endswith(';'):
812 string_search = re.compile(r'.*<!ENTITY.*' + string[1:-1] + '.*?"([^"]+?)".*>').search
814 if string_search is not None:
815 for xmlfile in xmlfiles:
816 for line in file(xmlfile):
817 matches = string_search(line)
819 return retrieve_string(app_dir, matches.group(1), xmlfiles)
822 return string.replace("\\'", "'")
825 # Return list of existing files that will be used to find the highest vercode
826 def manifest_paths(app_dir, flavour):
828 possible_manifests = \
829 [os.path.join(app_dir, 'AndroidManifest.xml'),
830 os.path.join(app_dir, 'src', 'main', 'AndroidManifest.xml'),
831 os.path.join(app_dir, 'src', 'AndroidManifest.xml'),
832 os.path.join(app_dir, 'build.gradle')]
835 possible_manifests.append(
836 os.path.join(app_dir, 'src', flavour, 'AndroidManifest.xml'))
838 return [path for path in possible_manifests if os.path.isfile(path)]
841 # Retrieve the package name. Returns the name, or None if not found.
842 def fetch_real_name(app_dir, flavour):
843 app_search = re.compile(r'.*<application.*').search
844 name_search = re.compile(r'.*android:label="([^"]+)".*').search
846 for f in manifest_paths(app_dir, flavour):
847 if not has_extension(f, 'xml'):
849 logging.debug("fetch_real_name: Checking manifest at " + f)
855 matches = name_search(line)
857 stringname = matches.group(1)
858 logging.debug("fetch_real_name: using string " + stringname)
859 result = retrieve_string(app_dir, stringname)
861 result = result.strip()
866 # Retrieve the version name
867 def version_name(original, app_dir, flavour):
868 for f in manifest_paths(app_dir, flavour):
869 if not has_extension(f, 'xml'):
871 string = retrieve_string(app_dir, original)
877 def get_library_references(root_dir):
879 proppath = os.path.join(root_dir, 'project.properties')
880 if not os.path.isfile(proppath):
882 with open(proppath) as f:
883 for line in f.readlines():
884 if not line.startswith('android.library.reference.'):
886 path = line.split('=')[1].strip()
887 relpath = os.path.join(root_dir, path)
888 if not os.path.isdir(relpath):
890 logging.debug("Found subproject at %s" % path)
891 libraries.append(path)
895 def ant_subprojects(root_dir):
896 subprojects = get_library_references(root_dir)
897 for subpath in subprojects:
898 subrelpath = os.path.join(root_dir, subpath)
899 for p in get_library_references(subrelpath):
900 relp = os.path.normpath(os.path.join(subpath, p))
901 if relp not in subprojects:
902 subprojects.insert(0, relp)
906 def remove_debuggable_flags(root_dir):
907 # Remove forced debuggable flags
908 logging.debug("Removing debuggable flags from %s" % root_dir)
909 for root, dirs, files in os.walk(root_dir):
910 if 'AndroidManifest.xml' in files:
911 path = os.path.join(root, 'AndroidManifest.xml')
912 p = SilentPopen(['sed', '-i', 's/android:debuggable="[^"]*"//g', path])
913 if p.returncode != 0:
914 raise BuildException("Failed to remove debuggable flags of %s" % path)
917 # Extract some information from the AndroidManifest.xml at the given path.
918 # Returns (version, vercode, package), any or all of which might be None.
919 # All values returned are strings.
920 def parse_androidmanifests(paths, ignoreversions=None):
923 return (None, None, None)
925 vcsearch = re.compile(r'.*:versionCode="([0-9]+?)".*').search
926 vnsearch = re.compile(r'.*:versionName="([^"]+?)".*').search
927 psearch = re.compile(r'.*package="([^"]+)".*').search
929 vcsearch_g = re.compile(r'.*versionCode *=* *["\']*([0-9]+)["\']*').search
930 vnsearch_g = re.compile(r'.*versionName *=* *(["\'])((?:(?=(\\?))\3.)*?)\1.*').search
931 psearch_g = re.compile(r'.*packageName *=* *["\']([^"]+)["\'].*').search
933 ignoresearch = re.compile(ignoreversions).search if ignoreversions else None
941 gradle = has_extension(path, 'gradle')
944 # Remember package name, may be defined separately from version+vercode
945 package = max_package
947 for line in file(path):
950 matches = psearch_g(line)
952 matches = psearch(line)
954 package = matches.group(1)
957 matches = vnsearch_g(line)
959 matches = vnsearch(line)
961 version = matches.group(2 if gradle else 1)
964 matches = vcsearch_g(line)
966 matches = vcsearch(line)
968 vercode = matches.group(1)
970 # Always grab the package name and version name in case they are not
971 # together with the highest version code
972 if max_package is None and package is not None:
973 max_package = package
974 if max_version is None and version is not None:
975 max_version = version
977 if max_vercode is None or (vercode is not None and vercode > max_vercode):
978 if not ignoresearch or not ignoresearch(version):
979 if version is not None:
980 max_version = version
981 if vercode is not None:
982 max_vercode = vercode
983 if package is not None:
984 max_package = package
986 max_version = "Ignore"
988 if max_version is None:
989 max_version = "Unknown"
991 return (max_version, max_vercode, max_package)
994 class FDroidException(Exception):
995 def __init__(self, value, detail=None):
999 def get_wikitext(self):
1000 ret = repr(self.value) + "\n"
1004 txt = self.detail[-8192:] if len(self.detail) > 8192 else self.detail
1012 ret += "\n==== detail begin ====\n%s\n==== detail end ====" % self.detail.strip()
1016 class VCSException(FDroidException):
1020 class BuildException(FDroidException):
1024 # Get the specified source library.
1025 # Returns the path to it. Normally this is the path to be used when referencing
1026 # it, which may be a subdirectory of the actual project. If you want the base
1027 # directory of the project, pass 'basepath=True'.
1028 def getsrclib(spec, srclib_dir, srclibpaths=[], subdir=None,
1029 basepath=False, raw=False, prepare=True, preponly=False):
1037 name, ref = spec.split('@')
1039 number, name = name.split(':', 1)
1041 name, subdir = name.split('/', 1)
1043 if name not in metadata.srclibs:
1044 raise VCSException('srclib ' + name + ' not found.')
1046 srclib = metadata.srclibs[name]
1048 sdir = os.path.join(srclib_dir, name)
1051 vcs = getvcs(srclib["Repo Type"], srclib["Repo"], sdir)
1052 vcs.srclib = (name, number, sdir)
1054 vcs.gotorevision(ref)
1061 libdir = os.path.join(sdir, subdir)
1062 elif srclib["Subdir"]:
1063 for subdir in srclib["Subdir"]:
1064 libdir_candidate = os.path.join(sdir, subdir)
1065 if os.path.exists(libdir_candidate):
1066 libdir = libdir_candidate
1072 if srclib["Srclibs"]:
1074 for lib in srclib["Srclibs"].replace(';', ',').split(','):
1076 for t in srclibpaths:
1081 raise VCSException('Missing recursive srclib %s for %s' % (
1083 place_srclib(libdir, n, s_tuple[2])
1086 remove_signing_keys(sdir)
1087 remove_debuggable_flags(sdir)
1091 if srclib["Prepare"]:
1092 cmd = replace_config_vars(srclib["Prepare"])
1094 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=libdir)
1095 if p.returncode != 0:
1096 raise BuildException("Error running prepare command for srclib %s"
1102 return (name, number, libdir)
1105 # Prepare the source code for a particular build
1106 # 'vcs' - the appropriate vcs object for the application
1107 # 'app' - the application details from the metadata
1108 # 'build' - the build details from the metadata
1109 # 'build_dir' - the path to the build directory, usually
1111 # 'srclib_dir' - the path to the source libraries directory, usually
1113 # 'extlib_dir' - the path to the external libraries directory, usually
1115 # Returns the (root, srclibpaths) where:
1116 # 'root' is the root directory, which may be the same as 'build_dir' or may
1117 # be a subdirectory of it.
1118 # 'srclibpaths' is information on the srclibs being used
1119 def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=False):
1121 # Optionally, the actual app source can be in a subdirectory
1123 root_dir = os.path.join(build_dir, build['subdir'])
1125 root_dir = build_dir
1127 # Get a working copy of the right revision
1128 logging.info("Getting source for revision " + build['commit'])
1129 vcs.gotorevision(build['commit'])
1131 # Initialise submodules if requred
1132 if build['submodules']:
1133 logging.info("Initialising submodules")
1134 vcs.initsubmodules()
1136 # Check that a subdir (if we're using one) exists. This has to happen
1137 # after the checkout, since it might not exist elsewhere
1138 if not os.path.exists(root_dir):
1139 raise BuildException('Missing subdir ' + root_dir)
1141 # Run an init command if one is required
1143 cmd = replace_config_vars(build['init'])
1144 logging.info("Running 'init' commands in %s" % root_dir)
1146 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
1147 if p.returncode != 0:
1148 raise BuildException("Error running init command for %s:%s" %
1149 (app['id'], build['version']), p.output)
1151 # Apply patches if any
1153 logging.info("Applying patches")
1154 for patch in build['patch']:
1155 patch = patch.strip()
1156 logging.info("Applying " + patch)
1157 patch_path = os.path.join('metadata', app['id'], patch)
1158 p = FDroidPopen(['patch', '-p1', '-i', os.path.abspath(patch_path)], cwd=build_dir)
1159 if p.returncode != 0:
1160 raise BuildException("Failed to apply patch %s" % patch_path)
1162 # Get required source libraries
1164 if build['srclibs']:
1165 logging.info("Collecting source libraries")
1166 for lib in build['srclibs']:
1167 srclibpaths.append(getsrclib(lib, srclib_dir, srclibpaths,
1170 for name, number, libpath in srclibpaths:
1171 place_srclib(root_dir, int(number) if number else None, libpath)
1173 basesrclib = vcs.getsrclib()
1174 # If one was used for the main source, add that too.
1176 srclibpaths.append(basesrclib)
1178 # Update the local.properties file
1179 localprops = [os.path.join(build_dir, 'local.properties')]
1181 localprops += [os.path.join(root_dir, 'local.properties')]
1182 for path in localprops:
1183 if not os.path.isfile(path):
1185 logging.info("Updating properties file at %s" % path)
1190 # Fix old-fashioned 'sdk-location' by copying
1191 # from sdk.dir, if necessary
1192 if build['oldsdkloc']:
1193 sdkloc = re.match(r".*^sdk.dir=(\S+)$.*", props,
1194 re.S | re.M).group(1)
1195 props += "sdk-location=%s\n" % sdkloc
1197 props += "sdk.dir=%s\n" % config['sdk_path']
1198 props += "sdk-location=%s\n" % config['sdk_path']
1199 if 'ndk_path' in config:
1201 props += "ndk.dir=%s\n" % config['ndk_path']
1202 props += "ndk-location=%s\n" % config['ndk_path']
1203 # Add java.encoding if necessary
1204 if build['encoding']:
1205 props += "java.encoding=%s\n" % build['encoding']
1211 if build['type'] == 'gradle':
1212 flavour = build['gradle']
1213 if flavour in ['main', 'yes', '']:
1216 version_regex = re.compile(r".*'com\.android\.tools\.build:gradle:([^\.]+\.[^\.]+).*'.*")
1217 gradlepluginver = None
1219 gradle_files = [os.path.join(root_dir, 'build.gradle')]
1221 # Parent dir build.gradle
1222 parent_dir = os.path.normpath(os.path.join(root_dir, '..'))
1223 if parent_dir.startswith(build_dir):
1224 gradle_files.append(os.path.join(parent_dir, 'build.gradle'))
1226 for path in gradle_files:
1229 if not os.path.isfile(path):
1231 with open(path) as f:
1233 match = version_regex.match(line)
1235 gradlepluginver = match.group(1)
1239 build['gradlepluginver'] = LooseVersion(gradlepluginver)
1241 logging.warn("Could not fetch the gradle plugin version, defaulting to 0.11")
1242 build['gradlepluginver'] = LooseVersion('0.11')
1245 n = build["target"].split('-')[1]
1246 SilentPopen(['sed', '-i',
1247 's@compileSdkVersion *[0-9]*@compileSdkVersion ' + n + '@g',
1251 # Remove forced debuggable flags
1252 remove_debuggable_flags(root_dir)
1254 # Insert version code and number into the manifest if necessary
1255 if build['forceversion']:
1256 logging.info("Changing the version name")
1257 for path in manifest_paths(root_dir, flavour):
1258 if not os.path.isfile(path):
1260 if has_extension(path, 'xml'):
1261 p = SilentPopen(['sed', '-i',
1262 's/android:versionName="[^"]*"/android:versionName="'
1263 + build['version'] + '"/g',
1265 if p.returncode != 0:
1266 raise BuildException("Failed to amend manifest")
1267 elif has_extension(path, 'gradle'):
1268 p = SilentPopen(['sed', '-i',
1269 's/versionName *=* *"[^"]*"/versionName = "'
1270 + build['version'] + '"/g',
1272 if p.returncode != 0:
1273 raise BuildException("Failed to amend build.gradle")
1274 if build['forcevercode']:
1275 logging.info("Changing the version code")
1276 for path in manifest_paths(root_dir, flavour):
1277 if not os.path.isfile(path):
1279 if has_extension(path, 'xml'):
1280 p = SilentPopen(['sed', '-i',
1281 's/android:versionCode="[^"]*"/android:versionCode="'
1282 + build['vercode'] + '"/g',
1284 if p.returncode != 0:
1285 raise BuildException("Failed to amend manifest")
1286 elif has_extension(path, 'gradle'):
1287 p = SilentPopen(['sed', '-i',
1288 's/versionCode *=* *[0-9]*/versionCode = '
1289 + build['vercode'] + '/g',
1291 if p.returncode != 0:
1292 raise BuildException("Failed to amend build.gradle")
1294 # Delete unwanted files
1296 logging.info("Removing specified files")
1297 for part in getpaths(build_dir, build, 'rm'):
1298 dest = os.path.join(build_dir, part)
1299 logging.info("Removing {0}".format(part))
1300 if os.path.lexists(dest):
1301 if os.path.islink(dest):
1302 SilentPopen(['unlink ' + dest], shell=True)
1304 SilentPopen(['rm -rf ' + dest], shell=True)
1306 logging.info("...but it didn't exist")
1308 remove_signing_keys(build_dir)
1310 # Add required external libraries
1311 if build['extlibs']:
1312 logging.info("Collecting prebuilt libraries")
1313 libsdir = os.path.join(root_dir, 'libs')
1314 if not os.path.exists(libsdir):
1316 for lib in build['extlibs']:
1318 logging.info("...installing extlib {0}".format(lib))
1319 libf = os.path.basename(lib)
1320 libsrc = os.path.join(extlib_dir, lib)
1321 if not os.path.exists(libsrc):
1322 raise BuildException("Missing extlib file {0}".format(libsrc))
1323 shutil.copyfile(libsrc, os.path.join(libsdir, libf))
1325 # Run a pre-build command if one is required
1326 if build['prebuild']:
1327 logging.info("Running 'prebuild' commands in %s" % root_dir)
1329 cmd = replace_config_vars(build['prebuild'])
1331 # Substitute source library paths into prebuild commands
1332 for name, number, libpath in srclibpaths:
1333 libpath = os.path.relpath(libpath, root_dir)
1334 cmd = cmd.replace('$$' + name + '$$', libpath)
1336 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
1337 if p.returncode != 0:
1338 raise BuildException("Error running prebuild command for %s:%s" %
1339 (app['id'], build['version']), p.output)
1341 # Generate (or update) the ant build file, build.xml...
1342 if build['update'] and build['update'] != ['no'] and build['type'] == 'ant':
1343 parms = [config['android'], 'update', 'lib-project']
1344 lparms = [config['android'], 'update', 'project']
1347 parms += ['-t', build['target']]
1348 lparms += ['-t', build['target']]
1349 if build['update'] == ['auto']:
1350 update_dirs = ant_subprojects(root_dir) + ['.']
1352 update_dirs = build['update']
1354 for d in update_dirs:
1355 subdir = os.path.join(root_dir, d)
1357 logging.debug("Updating main project")
1358 cmd = parms + ['-p', d]
1360 logging.debug("Updating subproject %s" % d)
1361 cmd = lparms + ['-p', d]
1362 p = FDroidPopen(cmd, cwd=root_dir)
1363 # Check to see whether an error was returned without a proper exit
1364 # code (this is the case for the 'no target set or target invalid'
1366 if p.returncode != 0 or p.output.startswith("Error: "):
1367 raise BuildException("Failed to update project at %s" % d, p.output)
1368 # Clean update dirs via ant
1370 logging.info("Cleaning subproject %s" % d)
1371 p = FDroidPopen(['ant', 'clean'], cwd=subdir)
1373 return (root_dir, srclibpaths)
1376 # Split and extend via globbing the paths from a field
1377 def getpaths(build_dir, build, field):
1379 for p in build[field]:
1381 full_path = os.path.join(build_dir, p)
1382 full_path = os.path.normpath(full_path)
1383 paths += [r[len(build_dir) + 1:] for r in glob.glob(full_path)]
1387 # Scan the source code in the given directory (and all subdirectories)
1388 # and return the number of fatal problems encountered
1389 def scan_source(build_dir, root_dir, thisbuild):
1393 # Common known non-free blobs (always lower case):
1395 re.compile(r'flurryagent', re.IGNORECASE),
1396 re.compile(r'paypal.*mpl', re.IGNORECASE),
1397 re.compile(r'google.*analytics', re.IGNORECASE),
1398 re.compile(r'admob.*sdk.*android', re.IGNORECASE),
1399 re.compile(r'google.*ad.*view', re.IGNORECASE),
1400 re.compile(r'google.*admob', re.IGNORECASE),
1401 re.compile(r'google.*play.*services', re.IGNORECASE),
1402 re.compile(r'crittercism', re.IGNORECASE),
1403 re.compile(r'heyzap', re.IGNORECASE),
1404 re.compile(r'jpct.*ae', re.IGNORECASE),
1405 re.compile(r'youtube.*android.*player.*api', re.IGNORECASE),
1406 re.compile(r'bugsense', re.IGNORECASE),
1407 re.compile(r'crashlytics', re.IGNORECASE),
1408 re.compile(r'ouya.*sdk', re.IGNORECASE),
1409 re.compile(r'libspen23', re.IGNORECASE),
1412 scanignore = getpaths(build_dir, thisbuild, 'scanignore')
1413 scandelete = getpaths(build_dir, thisbuild, 'scandelete')
1416 ms = magic.open(magic.MIME_TYPE)
1418 except AttributeError:
1422 for i in scanignore:
1423 if fd.startswith(i):
1428 for i in scandelete:
1429 if fd.startswith(i):
1433 def removeproblem(what, fd, fp):
1434 logging.info('Removing %s at %s' % (what, fd))
1437 def warnproblem(what, fd):
1438 logging.warn('Found %s at %s' % (what, fd))
1440 def handleproblem(what, fd, fp):
1442 logging.info('Ignoring %s at %s' % (what, fd))
1444 removeproblem(what, fd, fp)
1446 logging.error('Found %s at %s' % (what, fd))
1450 # Iterate through all files in the source code
1451 for r, d, f in os.walk(build_dir, topdown=True):
1453 # It's topdown, so checking the basename is enough
1454 for ignoredir in ('.hg', '.git', '.svn', '.bzr'):
1460 # Path (relative) to the file
1461 fp = os.path.join(r, curfile)
1462 fd = fp[len(build_dir) + 1:]
1465 mime = magic.from_file(fp, mime=True) if ms is None else ms.file(fp)
1466 except UnicodeError:
1467 warnproblem('malformed magic number', fd)
1469 if mime == 'application/x-sharedlib':
1470 count += handleproblem('shared library', fd, fp)
1472 elif mime == 'application/x-archive':
1473 count += handleproblem('static library', fd, fp)
1475 elif mime == 'application/x-executable':
1476 count += handleproblem('binary executable', fd, fp)
1478 elif mime == 'application/x-java-applet':
1479 count += handleproblem('Java compiled class', fd, fp)
1484 'application/java-archive',
1485 'application/octet-stream',
1489 if has_extension(fp, 'apk'):
1490 removeproblem('APK file', fd, fp)
1492 elif has_extension(fp, 'jar'):
1494 if any(suspect.match(curfile) for suspect in usual_suspects):
1495 count += handleproblem('usual supect', fd, fp)
1497 warnproblem('JAR file', fd)
1499 elif has_extension(fp, 'zip'):
1500 warnproblem('ZIP file', fd)
1503 warnproblem('unknown compressed or binary file', fd)
1505 elif has_extension(fp, 'java'):
1506 for line in file(fp):
1507 if 'DexClassLoader' in line:
1508 count += handleproblem('DexClassLoader', fd, fp)
1513 # Presence of a jni directory without buildjni=yes might
1514 # indicate a problem (if it's not a problem, explicitly use
1515 # buildjni=no to bypass this check)
1516 if (os.path.exists(os.path.join(root_dir, 'jni')) and
1517 not thisbuild['buildjni']):
1518 logging.error('Found jni directory, but buildjni is not enabled. Set it to \'no\' to ignore.')
1527 self.path = os.path.join('stats', 'known_apks.txt')
1529 if os.path.exists(self.path):
1530 for line in file(self.path):
1531 t = line.rstrip().split(' ')
1533 self.apks[t[0]] = (t[1], None)
1535 self.apks[t[0]] = (t[1], time.strptime(t[2], '%Y-%m-%d'))
1536 self.changed = False
1538 def writeifchanged(self):
1540 if not os.path.exists('stats'):
1542 f = open(self.path, 'w')
1544 for apk, app in self.apks.iteritems():
1546 line = apk + ' ' + appid
1548 line += ' ' + time.strftime('%Y-%m-%d', added)
1550 for line in sorted(lst):
1551 f.write(line + '\n')
1554 # Record an apk (if it's new, otherwise does nothing)
1555 # Returns the date it was added.
1556 def recordapk(self, apk, app):
1557 if apk not in self.apks:
1558 self.apks[apk] = (app, time.gmtime(time.time()))
1560 _, added = self.apks[apk]
1563 # Look up information - given the 'apkname', returns (app id, date added/None).
1564 # Or returns None for an unknown apk.
1565 def getapp(self, apkname):
1566 if apkname in self.apks:
1567 return self.apks[apkname]
1570 # Get the most recent 'num' apps added to the repo, as a list of package ids
1571 # with the most recent first.
1572 def getlatest(self, num):
1574 for apk, app in self.apks.iteritems():
1578 if apps[appid] > added:
1582 sortedapps = sorted(apps.iteritems(), key=operator.itemgetter(1))[-num:]
1583 lst = [app for app, _ in sortedapps]
1588 def isApkDebuggable(apkfile, config):
1589 """Returns True if the given apk file is debuggable
1591 :param apkfile: full path to the apk to check"""
1593 p = SilentPopen([os.path.join(config['sdk_path'], 'build-tools',
1594 config['build_tools'], 'aapt'),
1595 'dump', 'xmltree', apkfile, 'AndroidManifest.xml'])
1596 if p.returncode != 0:
1597 logging.critical("Failed to get apk manifest information")
1599 for line in p.output.splitlines():
1600 if 'android:debuggable' in line and not line.endswith('0x0'):
1605 class AsynchronousFileReader(threading.Thread):
1607 Helper class to implement asynchronous reading of a file
1608 in a separate thread. Pushes read lines on a queue to
1609 be consumed in another thread.
1612 def __init__(self, fd, queue):
1613 assert isinstance(queue, Queue.Queue)
1614 assert callable(fd.readline)
1615 threading.Thread.__init__(self)
1620 '''The body of the tread: read lines and put them on the queue.'''
1621 for line in iter(self._fd.readline, ''):
1622 self._queue.put(line)
1625 '''Check whether there is no more content to expect.'''
1626 return not self.is_alive() and self._queue.empty()
1634 def SilentPopen(commands, cwd=None, shell=False):
1635 return FDroidPopen(commands, cwd=cwd, shell=shell, output=False)
1638 def FDroidPopen(commands, cwd=None, shell=False, output=True):
1640 Run a command and capture the possibly huge output.
1642 :param commands: command and argument list like in subprocess.Popen
1643 :param cwd: optionally specifies a working directory
1644 :returns: A PopenResult.
1650 cwd = os.path.normpath(cwd)
1651 logging.debug("Directory: %s" % cwd)
1652 logging.debug("> %s" % ' '.join(commands))
1654 result = PopenResult()
1655 p = subprocess.Popen(commands, cwd=cwd, shell=shell, env=env,
1656 stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
1658 stdout_queue = Queue.Queue()
1659 stdout_reader = AsynchronousFileReader(p.stdout, stdout_queue)
1660 stdout_reader.start()
1662 # Check the queue for output (until there is no more to get)
1663 while not stdout_reader.eof():
1664 while not stdout_queue.empty():
1665 line = stdout_queue.get()
1666 if output and options.verbose:
1667 # Output directly to console
1668 sys.stderr.write(line)
1670 result.output += line
1674 result.returncode = p.wait()
1678 def remove_signing_keys(build_dir):
1679 comment = re.compile(r'[ ]*//')
1680 signing_configs = re.compile(r'^[\t ]*signingConfigs[ \t]*{[ \t]*$')
1682 re.compile(r'^[\t ]*signingConfig [^ ]*$'),
1683 re.compile(r'.*android\.signingConfigs\.[^{]*$'),
1684 re.compile(r'.*variant\.outputFile = .*'),
1685 re.compile(r'.*\.readLine\(.*'),
1687 for root, dirs, files in os.walk(build_dir):
1688 if 'build.gradle' in files:
1689 path = os.path.join(root, 'build.gradle')
1691 with open(path, "r") as o:
1692 lines = o.readlines()
1697 with open(path, "w") as o:
1699 if comment.match(line):
1703 opened += line.count('{')
1704 opened -= line.count('}')
1707 if signing_configs.match(line):
1712 if any(s.match(line) for s in line_matches):
1720 logging.info("Cleaned build.gradle of keysigning configs at %s" % path)
1723 'project.properties',
1725 'default.properties',
1728 if propfile in files:
1729 path = os.path.join(root, propfile)
1731 with open(path, "r") as o:
1732 lines = o.readlines()
1736 with open(path, "w") as o:
1738 if any(line.startswith(s) for s in ('key.store', 'key.alias')):
1745 logging.info("Cleaned %s of keysigning configs at %s" % (propfile, path))
1748 def replace_config_vars(cmd):
1749 cmd = cmd.replace('$$SDK$$', config['sdk_path'])
1750 cmd = cmd.replace('$$NDK$$', config['ndk_path'])
1751 cmd = cmd.replace('$$MVN3$$', config['mvn3'])
1755 def place_srclib(root_dir, number, libpath):
1758 relpath = os.path.relpath(libpath, root_dir)
1759 proppath = os.path.join(root_dir, 'project.properties')
1762 if os.path.isfile(proppath):
1763 with open(proppath, "r") as o:
1764 lines = o.readlines()
1766 with open(proppath, "w") as o:
1769 if line.startswith('android.library.reference.%d=' % number):
1770 o.write('android.library.reference.%d=%s\n' % (number, relpath))
1775 o.write('android.library.reference.%d=%s\n' % (number, relpath))