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,
55 'stats_to_carbon': False,
57 'build_server_always': False,
58 'keystore': os.path.join(os.getenv("HOME"), '.local', 'share', 'fdroidserver', 'keystore.jks'),
59 'smartcardoptions': [],
65 'repo_url': "https://MyFirstFDroidRepo.org/fdroid/repo",
66 'repo_name': "My First FDroid Repo Demo",
67 'repo_icon': "fdroid-icon.png",
68 'repo_description': '''
69 This is a repository of apps to be used with FDroid. Applications in this
70 repository are either official binaries built by the original application
71 developers, or are binaries built from source by the admin of f-droid.org
72 using the tools on https://gitlab.com/u/fdroid.
78 def read_config(opts, config_file='config.py'):
79 """Read the repository config
81 The config is read from config_file, which is in the current directory when
82 any of the repo management commands are used.
84 global config, options, env
86 if config is not None:
88 if not os.path.isfile(config_file):
89 logging.critical("Missing config file - is this a repo directory?")
96 logging.debug("Reading %s" % config_file)
97 execfile(config_file, config)
99 # smartcardoptions must be a list since its command line args for Popen
100 if 'smartcardoptions' in config:
101 config['smartcardoptions'] = config['smartcardoptions'].split(' ')
102 elif 'keystore' in config and config['keystore'] == 'NONE':
103 # keystore='NONE' means use smartcard, these are required defaults
104 config['smartcardoptions'] = ['-storetype', 'PKCS11', '-providerName',
105 'SunPKCS11-OpenSC', '-providerClass',
106 'sun.security.pkcs11.SunPKCS11',
107 '-providerArg', 'opensc-fdroid.cfg']
109 if any(k in config for k in ["keystore", "keystorepass", "keypass"]):
110 st = os.stat(config_file)
111 if st.st_mode & stat.S_IRWXG or st.st_mode & stat.S_IRWXO:
112 logging.warn("unsafe permissions on {0} (should be 0600)!".format(config_file))
114 defconfig = get_default_config()
115 for k, v in defconfig.items():
119 # Expand environment variables
120 for k, v in config.items():
123 v = os.path.expanduser(v)
124 config[k] = os.path.expandvars(v)
126 if not test_sdk_exists(config):
129 if not test_build_tools_exists(config):
134 os.path.join(config['sdk_path'], 'build-tools', config['build_tools'], 'aapt'),
137 os.path.join(config['sdk_path'], 'tools', 'zipalign'),
138 os.path.join(config['sdk_path'], 'build-tools', config['build_tools'], 'zipalign'),
141 os.path.join(config['sdk_path'], 'tools', 'android'),
144 os.path.join(config['sdk_path'], 'platform-tools', 'adb'),
148 for b, paths in bin_paths.items():
151 if os.path.isfile(path):
154 if config[b] is None:
155 logging.warn("Could not find %s in any of the following paths:\n%s" % (
156 b, '\n'.join(paths)))
158 # There is no standard, so just set up the most common environment
161 for n in ['ANDROID_HOME', 'ANDROID_SDK']:
162 env[n] = config['sdk_path']
163 for n in ['ANDROID_NDK', 'NDK']:
164 env[n] = config['ndk_path']
166 for k in ["keystorepass", "keypass"]:
168 write_password_file(k)
170 for k in ["repo_description", "archive_description"]:
172 config[k] = clean_description(config[k])
174 if 'serverwebroot' in config:
175 if isinstance(config['serverwebroot'], basestring):
176 roots = [config['serverwebroot']]
177 elif all(isinstance(item, basestring) for item in config['serverwebroot']):
178 roots = config['serverwebroot']
180 raise TypeError('only accepts strings, lists, and tuples')
182 for rootstr in roots:
183 # since this is used with rsync, where trailing slashes have
184 # meaning, ensure there is always a trailing slash
185 if rootstr[-1] != '/':
187 rootlist.append(rootstr.replace('//', '/'))
188 config['serverwebroot'] = rootlist
193 def test_sdk_exists(c):
194 if c['sdk_path'] is None:
195 # c['sdk_path'] is set to the value of ANDROID_HOME by default
196 logging.error('No Android SDK found! ANDROID_HOME is not set and sdk_path is not in config.py!')
197 logging.error('You can use ANDROID_HOME to set the path to your SDK, i.e.:')
198 logging.error('\texport ANDROID_HOME=/opt/android-sdk')
200 if not os.path.exists(c['sdk_path']):
201 logging.critical('Android SDK path "' + c['sdk_path'] + '" does not exist!')
203 if not os.path.isdir(c['sdk_path']):
204 logging.critical('Android SDK path "' + c['sdk_path'] + '" is not a directory!')
206 for d in ['build-tools', 'platform-tools', 'tools']:
207 if not os.path.isdir(os.path.join(c['sdk_path'], d)):
208 logging.critical('Android SDK path "%s" does not contain "%s/"!' % (
214 def test_build_tools_exists(c):
215 if not test_sdk_exists(c):
217 build_tools = os.path.join(c['sdk_path'], 'build-tools')
218 versioned_build_tools = os.path.join(build_tools, c['build_tools'])
219 if not os.path.isdir(versioned_build_tools):
220 logging.critical('Android Build Tools path "'
221 + versioned_build_tools + '" does not exist!')
226 def write_password_file(pwtype, password=None):
228 writes out passwords to a protected file instead of passing passwords as
229 command line argments
231 filename = '.fdroid.' + pwtype + '.txt'
232 fd = os.open(filename, os.O_CREAT | os.O_TRUNC | os.O_WRONLY, 0600)
234 os.write(fd, config[pwtype])
236 os.write(fd, password)
238 config[pwtype + 'file'] = filename
241 # Given the arguments in the form of multiple appid:[vc] strings, this returns
242 # a dictionary with the set of vercodes specified for each package.
243 def read_pkg_args(args, allow_vercodes=False):
250 if allow_vercodes and ':' in p:
251 package, vercode = p.split(':')
253 package, vercode = p, None
254 if package not in vercodes:
255 vercodes[package] = [vercode] if vercode else []
257 elif vercode and vercode not in vercodes[package]:
258 vercodes[package] += [vercode] if vercode else []
263 # On top of what read_pkg_args does, this returns the whole app metadata, but
264 # limiting the builds list to the builds matching the vercodes specified.
265 def read_app_args(args, allapps, allow_vercodes=False):
267 vercodes = read_pkg_args(args, allow_vercodes)
273 for appid, app in allapps.iteritems():
274 if appid in vercodes:
277 if len(apps) != len(vercodes):
280 logging.critical("No such package: %s" % p)
281 raise FDroidException("Found invalid app ids in arguments")
283 raise FDroidException("No packages specified")
286 for appid, app in apps.iteritems():
290 app['builds'] = [b for b in app['builds'] if b['vercode'] in vc]
291 if len(app['builds']) != len(vercodes[appid]):
293 allvcs = [b['vercode'] for b in app['builds']]
294 for v in vercodes[appid]:
296 logging.critical("No such vercode %s for app %s" % (v, appid))
299 raise FDroidException("Found invalid vercodes for some apps")
304 def has_extension(filename, extension):
305 name, ext = os.path.splitext(filename)
306 ext = ext.lower()[1:]
307 return ext == extension
312 def clean_description(description):
313 'Remove unneeded newlines and spaces from a block of description text'
315 # this is split up by paragraph to make removing the newlines easier
316 for paragraph in re.split(r'\n\n', description):
317 paragraph = re.sub('\r', '', paragraph)
318 paragraph = re.sub('\n', ' ', paragraph)
319 paragraph = re.sub(' {2,}', ' ', paragraph)
320 paragraph = re.sub('^\s*(\w)', r'\1', paragraph)
321 returnstring += paragraph + '\n\n'
322 return returnstring.rstrip('\n')
325 def apknameinfo(filename):
327 filename = os.path.basename(filename)
328 if apk_regex is None:
329 apk_regex = re.compile(r"^(.+)_([0-9]+)\.apk$")
330 m = apk_regex.match(filename)
332 result = (m.group(1), m.group(2))
333 except AttributeError:
334 raise FDroidException("Invalid apk name: %s" % filename)
338 def getapkname(app, build):
339 return "%s_%s.apk" % (app['id'], build['vercode'])
342 def getsrcname(app, build):
343 return "%s_%s_src.tar.gz" % (app['id'], build['vercode'])
350 return app['Auto Name']
355 return '%s (%s)' % (app['Current Version'], app['Current Version Code'])
358 def getvcs(vcstype, remote, local):
360 return vcs_git(remote, local)
361 if vcstype == 'git-svn':
362 return vcs_gitsvn(remote, local)
364 return vcs_hg(remote, local)
366 return vcs_bzr(remote, local)
367 if vcstype == 'srclib':
368 if local != os.path.join('build', 'srclib', remote):
369 raise VCSException("Error: srclib paths are hard-coded!")
370 return getsrclib(remote, os.path.join('build', 'srclib'), raw=True)
372 raise VCSException("Deprecated vcs type 'svn' - please use 'git-svn' instead")
373 raise VCSException("Invalid vcs type " + vcstype)
376 def getsrclibvcs(name):
377 if name not in metadata.srclibs:
378 raise VCSException("Missing srclib " + name)
379 return metadata.srclibs[name]['Repo Type']
383 def __init__(self, remote, local):
385 # svn, git-svn and bzr may require auth
387 if self.repotype() in ('git-svn', 'bzr'):
389 self.username, remote = remote.split('@')
390 if ':' not in self.username:
391 raise VCSException("Password required with username")
392 self.username, self.password = self.username.split(':')
396 self.clone_failed = False
397 self.refreshed = False
403 # Take the local repository to a clean version of the given revision, which
404 # is specificed in the VCS's native format. Beforehand, the repository can
405 # be dirty, or even non-existent. If the repository does already exist
406 # locally, it will be updated from the origin, but only once in the
407 # lifetime of the vcs object.
408 # None is acceptable for 'rev' if you know you are cloning a clean copy of
409 # the repo - otherwise it must specify a valid revision.
410 def gotorevision(self, rev):
412 if self.clone_failed:
413 raise VCSException("Downloading the repository already failed once, not trying again.")
415 # The .fdroidvcs-id file for a repo tells us what VCS type
416 # and remote that directory was created from, allowing us to drop it
417 # automatically if either of those things changes.
418 fdpath = os.path.join(self.local, '..',
419 '.fdroidvcs-' + os.path.basename(self.local))
420 cdata = self.repotype() + ' ' + self.remote
423 if os.path.exists(self.local):
424 if os.path.exists(fdpath):
425 with open(fdpath, 'r') as f:
426 fsdata = f.read().strip()
432 "Repository details for %s changed - deleting" % (
436 logging.info("Repository details for %s missing - deleting" % (
439 shutil.rmtree(self.local)
444 self.gotorevisionx(rev)
445 except FDroidException, e:
448 # If necessary, write the .fdroidvcs file.
449 if writeback and not self.clone_failed:
450 with open(fdpath, 'w') as f:
456 # Derived classes need to implement this. It's called once basic checking
457 # has been performend.
458 def gotorevisionx(self, rev):
459 raise VCSException("This VCS type doesn't define gotorevisionx")
461 # Initialise and update submodules
462 def initsubmodules(self):
463 raise VCSException('Submodules not supported for this vcs type')
465 # Get a list of all known tags
467 raise VCSException('gettags not supported for this vcs type')
469 # Get a list of latest number tags
470 def latesttags(self, number):
471 raise VCSException('latesttags not supported for this vcs type')
473 # Get current commit reference (hash, revision, etc)
475 raise VCSException('getref not supported for this vcs type')
477 # Returns the srclib (name, path) used in setting up the current
488 # If the local directory exists, but is somehow not a git repository, git
489 # will traverse up the directory tree until it finds one that is (i.e.
490 # fdroidserver) and then we'll proceed to destroy it! This is called as
493 p = SilentPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local)
494 result = p.output.rstrip()
495 if not result.endswith(self.local):
496 raise VCSException('Repository mismatch')
498 def gotorevisionx(self, rev):
499 if not os.path.exists(self.local):
501 p = FDroidPopen(['git', 'clone', self.remote, self.local])
502 if p.returncode != 0:
503 self.clone_failed = True
504 raise VCSException("Git clone failed", p.output)
508 # Discard any working tree changes
509 p = SilentPopen(['git', 'reset', '--hard'], cwd=self.local)
510 if p.returncode != 0:
511 raise VCSException("Git reset failed", p.output)
512 # Remove untracked files now, in case they're tracked in the target
513 # revision (it happens!)
514 p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
515 if p.returncode != 0:
516 raise VCSException("Git clean failed", p.output)
517 if not self.refreshed:
518 # Get latest commits and tags from remote
519 p = FDroidPopen(['git', 'fetch', 'origin'], cwd=self.local)
520 if p.returncode != 0:
521 raise VCSException("Git fetch failed", p.output)
522 p = SilentPopen(['git', 'fetch', '--prune', '--tags', 'origin'], cwd=self.local)
523 if p.returncode != 0:
524 raise VCSException("Git fetch failed", p.output)
525 # Recreate origin/HEAD as git clone would do it, in case it disappeared
526 p = SilentPopen(['git', 'remote', 'set-head', 'origin', '--auto'], cwd=self.local)
527 if p.returncode != 0:
528 lines = p.output.splitlines()
529 if 'Multiple remote HEAD branches' not in lines[0]:
530 raise VCSException("Git remote set-head failed", p.output)
531 branch = lines[1].split(' ')[-1]
532 p2 = SilentPopen(['git', 'remote', 'set-head', 'origin', branch], cwd=self.local)
533 if p2.returncode != 0:
534 raise VCSException("Git remote set-head failed", p.output + '\n' + p2.output)
535 self.refreshed = True
536 # origin/HEAD is the HEAD of the remote, e.g. the "default branch" on
537 # a github repo. Most of the time this is the same as origin/master.
538 rev = rev or 'origin/HEAD'
539 p = SilentPopen(['git', 'checkout', '-f', rev], cwd=self.local)
540 if p.returncode != 0:
541 raise VCSException("Git checkout of '%s' failed" % rev, p.output)
542 # Get rid of any uncontrolled files left behind
543 p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
544 if p.returncode != 0:
545 raise VCSException("Git clean failed", p.output)
547 def initsubmodules(self):
549 submfile = os.path.join(self.local, '.gitmodules')
550 if not os.path.isfile(submfile):
551 raise VCSException("No git submodules available")
553 # fix submodules not accessible without an account and public key auth
554 with open(submfile, 'r') as f:
555 lines = f.readlines()
556 with open(submfile, 'w') as f:
558 if 'git@github.com' in line:
559 line = line.replace('git@github.com:', 'https://github.com/')
563 ['git', 'reset', '--hard'],
564 ['git', 'clean', '-dffx'],
566 p = SilentPopen(['git', 'submodule', 'foreach', '--recursive'] + cmd, cwd=self.local)
567 if p.returncode != 0:
568 raise VCSException("Git submodule reset failed", p.output)
569 p = SilentPopen(['git', 'submodule', 'sync'], cwd=self.local)
570 if p.returncode != 0:
571 raise VCSException("Git submodule sync failed", p.output)
572 p = FDroidPopen(['git', 'submodule', 'update', '--init', '--force', '--recursive'], cwd=self.local)
573 if p.returncode != 0:
574 raise VCSException("Git submodule update failed", p.output)
578 p = SilentPopen(['git', 'tag'], cwd=self.local)
579 return p.output.splitlines()
581 def latesttags(self, alltags, number):
583 p = SilentPopen(['echo "' + '\n'.join(alltags) + '" | '
584 + 'xargs -I@ git log --format=format:"%at @%n" -1 @ | '
585 + 'sort -n | awk \'{print $2}\''],
586 cwd=self.local, shell=True)
587 return p.output.splitlines()[-number:]
590 class vcs_gitsvn(vcs):
595 # Damn git-svn tries to use a graphical password prompt, so we have to
596 # trick it into taking the password from stdin
598 if self.username is None:
600 return ('echo "%s" | DISPLAY="" ' % self.password, ' --username "%s"' % self.username)
602 # If the local directory exists, but is somehow not a git repository, git
603 # will traverse up the directory tree until it finds one that is (i.e.
604 # fdroidserver) and then we'll proceed to destory it! This is called as
607 p = SilentPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local)
608 result = p.output.rstrip()
609 if not result.endswith(self.local):
610 raise VCSException('Repository mismatch')
612 def gotorevisionx(self, rev):
613 if not os.path.exists(self.local):
615 gitsvn_cmd = '%sgit svn clone%s' % self.userargs()
616 if ';' in self.remote:
617 remote_split = self.remote.split(';')
618 for i in remote_split[1:]:
619 if i.startswith('trunk='):
620 gitsvn_cmd += ' -T %s' % i[6:]
621 elif i.startswith('tags='):
622 gitsvn_cmd += ' -t %s' % i[5:]
623 elif i.startswith('branches='):
624 gitsvn_cmd += ' -b %s' % i[9:]
625 p = SilentPopen([gitsvn_cmd + " %s %s" % (remote_split[0], self.local)], shell=True)
626 if p.returncode != 0:
627 self.clone_failed = True
628 raise VCSException("Git svn clone failed", p.output)
630 p = SilentPopen([gitsvn_cmd + " %s %s" % (self.remote, self.local)], shell=True)
631 if p.returncode != 0:
632 self.clone_failed = True
633 raise VCSException("Git svn clone failed", p.output)
637 # Discard any working tree changes
638 p = SilentPopen(['git', 'reset', '--hard'], cwd=self.local)
639 if p.returncode != 0:
640 raise VCSException("Git reset failed", p.output)
641 # Remove untracked files now, in case they're tracked in the target
642 # revision (it happens!)
643 p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
644 if p.returncode != 0:
645 raise VCSException("Git clean failed", p.output)
646 if not self.refreshed:
647 # Get new commits, branches and tags from repo
648 p = SilentPopen(['%sgit svn fetch %s' % self.userargs()], cwd=self.local, shell=True)
649 if p.returncode != 0:
650 raise VCSException("Git svn fetch failed")
651 p = SilentPopen(['%sgit svn rebase %s' % self.userargs()], cwd=self.local, shell=True)
652 if p.returncode != 0:
653 raise VCSException("Git svn rebase failed", p.output)
654 self.refreshed = True
656 rev = rev or 'master'
658 nospaces_rev = rev.replace(' ', '%20')
659 # Try finding a svn tag
660 for treeish in ['origin/', '']:
661 p = SilentPopen(['git', 'checkout', treeish + 'tags/' + nospaces_rev],
663 if p.returncode == 0:
665 if p.returncode != 0:
666 # No tag found, normal svn rev translation
667 # Translate svn rev into git format
668 rev_split = rev.split('/')
671 for treeish in ['origin/', '']:
672 if len(rev_split) > 1:
673 treeish += rev_split[0]
674 svn_rev = rev_split[1]
677 # if no branch is specified, then assume trunk (i.e. 'master' branch):
681 svn_rev = svn_rev if svn_rev[0] == 'r' else 'r' + svn_rev
683 p = SilentPopen(['git', 'svn', 'find-rev', '--before', svn_rev, treeish],
685 git_rev = p.output.rstrip()
687 if p.returncode == 0 and git_rev:
690 if p.returncode != 0 or not git_rev:
691 # Try a plain git checkout as a last resort
692 p = SilentPopen(['git', 'checkout', rev], cwd=self.local)
693 if p.returncode != 0:
694 raise VCSException("No git treeish found and direct git checkout of '%s' failed" % rev, p.output)
696 # Check out the git rev equivalent to the svn rev
697 p = SilentPopen(['git', 'checkout', git_rev], cwd=self.local)
698 if p.returncode != 0:
699 raise VCSException("Git checkout of '%s' failed" % rev, p.output)
701 # Get rid of any uncontrolled files left behind
702 p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
703 if p.returncode != 0:
704 raise VCSException("Git clean failed", p.output)
708 for treeish in ['origin/', '']:
709 d = os.path.join(self.local, '.git', 'svn', 'refs', 'remotes', treeish, 'tags')
715 p = SilentPopen(['git', 'svn', 'find-rev', 'HEAD'], cwd=self.local)
716 if p.returncode != 0:
718 return p.output.strip()
726 def gotorevisionx(self, rev):
727 if not os.path.exists(self.local):
728 p = SilentPopen(['hg', 'clone', self.remote, self.local])
729 if p.returncode != 0:
730 self.clone_failed = True
731 raise VCSException("Hg clone failed", p.output)
733 p = SilentPopen(['hg status -uS | xargs rm -rf'], cwd=self.local, shell=True)
734 if p.returncode != 0:
735 raise VCSException("Hg clean failed", p.output)
736 if not self.refreshed:
737 p = SilentPopen(['hg', 'pull'], cwd=self.local)
738 if p.returncode != 0:
739 raise VCSException("Hg pull failed", p.output)
740 self.refreshed = True
742 rev = rev or 'default'
745 p = SilentPopen(['hg', 'update', '-C', rev], cwd=self.local)
746 if p.returncode != 0:
747 raise VCSException("Hg checkout of '%s' failed" % rev, p.output)
748 p = SilentPopen(['hg', 'purge', '--all'], cwd=self.local)
749 # Also delete untracked files, we have to enable purge extension for that:
750 if "'purge' is provided by the following extension" in p.output:
751 with open(os.path.join(self.local, '.hg', 'hgrc'), "a") as myfile:
752 myfile.write("\n[extensions]\nhgext.purge=\n")
753 p = SilentPopen(['hg', 'purge', '--all'], cwd=self.local)
754 if p.returncode != 0:
755 raise VCSException("HG purge failed", p.output)
756 elif p.returncode != 0:
757 raise VCSException("HG purge failed", p.output)
760 p = SilentPopen(['hg', 'tags', '-q'], cwd=self.local)
761 return p.output.splitlines()[1:]
769 def gotorevisionx(self, rev):
770 if not os.path.exists(self.local):
771 p = SilentPopen(['bzr', 'branch', self.remote, self.local])
772 if p.returncode != 0:
773 self.clone_failed = True
774 raise VCSException("Bzr branch failed", p.output)
776 p = SilentPopen(['bzr', 'clean-tree', '--force', '--unknown', '--ignored'], cwd=self.local)
777 if p.returncode != 0:
778 raise VCSException("Bzr revert failed", p.output)
779 if not self.refreshed:
780 p = SilentPopen(['bzr', 'pull'], cwd=self.local)
781 if p.returncode != 0:
782 raise VCSException("Bzr update failed", p.output)
783 self.refreshed = True
785 revargs = list(['-r', rev] if rev else [])
786 p = SilentPopen(['bzr', 'revert'] + revargs, cwd=self.local)
787 if p.returncode != 0:
788 raise VCSException("Bzr revert of '%s' failed" % rev, p.output)
791 p = SilentPopen(['bzr', 'tags'], cwd=self.local)
792 return [tag.split(' ')[0].strip() for tag in
793 p.output.splitlines()]
796 def retrieve_string(app_dir, string, xmlfiles=None):
799 os.path.join(app_dir, 'res'),
800 os.path.join(app_dir, 'src', 'main'),
805 for res_dir in res_dirs:
806 for r, d, f in os.walk(res_dir):
807 if os.path.basename(r) == 'values':
808 xmlfiles += [os.path.join(r, x) for x in f if x.endswith('.xml')]
811 if string.startswith('@string/'):
812 string_search = re.compile(r'.*name="' + string[8:] + '".*?>"?([^<]+?)"?<.*').search
813 elif string.startswith('&') and string.endswith(';'):
814 string_search = re.compile(r'.*<!ENTITY.*' + string[1:-1] + '.*?"([^"]+?)".*>').search
816 if string_search is not None:
817 for xmlfile in xmlfiles:
818 for line in file(xmlfile):
819 matches = string_search(line)
821 return retrieve_string(app_dir, matches.group(1), xmlfiles)
824 return string.replace("\\'", "'")
827 # Return list of existing files that will be used to find the highest vercode
828 def manifest_paths(app_dir, flavours):
830 possible_manifests = \
831 [os.path.join(app_dir, 'AndroidManifest.xml'),
832 os.path.join(app_dir, 'src', 'main', 'AndroidManifest.xml'),
833 os.path.join(app_dir, 'src', 'AndroidManifest.xml'),
834 os.path.join(app_dir, 'build.gradle')]
836 for flavour in flavours:
837 possible_manifests.append(
838 os.path.join(app_dir, 'src', flavour, 'AndroidManifest.xml'))
840 return [path for path in possible_manifests if os.path.isfile(path)]
843 # Retrieve the package name. Returns the name, or None if not found.
844 def fetch_real_name(app_dir, flavours):
845 app_search = re.compile(r'.*<application.*').search
846 name_search = re.compile(r'.*android:label="([^"]+)".*').search
848 for f in manifest_paths(app_dir, flavours):
849 if not has_extension(f, 'xml'):
851 logging.debug("fetch_real_name: Checking manifest at " + f)
857 matches = name_search(line)
859 stringname = matches.group(1)
860 logging.debug("fetch_real_name: using string " + stringname)
861 result = retrieve_string(app_dir, stringname)
863 result = result.strip()
868 # Retrieve the version name
869 def version_name(original, app_dir, flavours):
870 for f in manifest_paths(app_dir, flavours):
871 if not has_extension(f, 'xml'):
873 string = retrieve_string(app_dir, original)
879 def get_library_references(root_dir):
881 proppath = os.path.join(root_dir, 'project.properties')
882 if not os.path.isfile(proppath):
884 with open(proppath) as f:
885 for line in f.readlines():
886 if not line.startswith('android.library.reference.'):
888 path = line.split('=')[1].strip()
889 relpath = os.path.join(root_dir, path)
890 if not os.path.isdir(relpath):
892 logging.debug("Found subproject at %s" % path)
893 libraries.append(path)
897 def ant_subprojects(root_dir):
898 subprojects = get_library_references(root_dir)
899 for subpath in subprojects:
900 subrelpath = os.path.join(root_dir, subpath)
901 for p in get_library_references(subrelpath):
902 relp = os.path.normpath(os.path.join(subpath, p))
903 if relp not in subprojects:
904 subprojects.insert(0, relp)
908 def remove_debuggable_flags(root_dir):
909 # Remove forced debuggable flags
910 logging.debug("Removing debuggable flags from %s" % root_dir)
911 for root, dirs, files in os.walk(root_dir):
912 if 'AndroidManifest.xml' in files:
913 path = os.path.join(root, 'AndroidManifest.xml')
914 p = SilentPopen(['sed', '-i', 's/android:debuggable="[^"]*"//g', path])
915 if p.returncode != 0:
916 raise BuildException("Failed to remove debuggable flags of %s" % path)
919 # Extract some information from the AndroidManifest.xml at the given path.
920 # Returns (version, vercode, package), any or all of which might be None.
921 # All values returned are strings.
922 def parse_androidmanifests(paths, ignoreversions=None):
925 return (None, None, None)
927 vcsearch = re.compile(r'.*:versionCode="([0-9]+?)".*').search
928 vnsearch = re.compile(r'.*:versionName="([^"]+?)".*').search
929 psearch = re.compile(r'.*package="([^"]+)".*').search
931 vcsearch_g = re.compile(r'.*versionCode *=* *["\']*([0-9]+)["\']*').search
932 vnsearch_g = re.compile(r'.*versionName *=* *(["\'])((?:(?=(\\?))\3.)*?)\1.*').search
933 psearch_g = re.compile(r'.*packageName *=* *["\']([^"]+)["\'].*').search
935 ignoresearch = re.compile(ignoreversions).search if ignoreversions else None
943 gradle = has_extension(path, 'gradle')
946 # Remember package name, may be defined separately from version+vercode
947 package = max_package
949 for line in file(path):
952 matches = psearch_g(line)
954 matches = psearch(line)
956 package = matches.group(1)
959 matches = vnsearch_g(line)
961 matches = vnsearch(line)
963 version = matches.group(2 if gradle else 1)
966 matches = vcsearch_g(line)
968 matches = vcsearch(line)
970 vercode = matches.group(1)
972 # Always grab the package name and version name in case they are not
973 # together with the highest version code
974 if max_package is None and package is not None:
975 max_package = package
976 if max_version is None and version is not None:
977 max_version = version
979 if max_vercode is None or (vercode is not None and vercode > max_vercode):
980 if not ignoresearch or not ignoresearch(version):
981 if version is not None:
982 max_version = version
983 if vercode is not None:
984 max_vercode = vercode
985 if package is not None:
986 max_package = package
988 max_version = "Ignore"
990 if max_version is None:
991 max_version = "Unknown"
993 return (max_version, max_vercode, max_package)
996 class FDroidException(Exception):
997 def __init__(self, value, detail=None):
1001 def get_wikitext(self):
1002 ret = repr(self.value) + "\n"
1006 txt = self.detail[-8192:] if len(self.detail) > 8192 else self.detail
1014 ret += "\n==== detail begin ====\n%s\n==== detail end ====" % self.detail.strip()
1018 class VCSException(FDroidException):
1022 class BuildException(FDroidException):
1026 # Get the specified source library.
1027 # Returns the path to it. Normally this is the path to be used when referencing
1028 # it, which may be a subdirectory of the actual project. If you want the base
1029 # directory of the project, pass 'basepath=True'.
1030 def getsrclib(spec, srclib_dir, srclibpaths=[], subdir=None,
1031 basepath=False, raw=False, prepare=True, preponly=False):
1039 name, ref = spec.split('@')
1041 number, name = name.split(':', 1)
1043 name, subdir = name.split('/', 1)
1045 if name not in metadata.srclibs:
1046 raise VCSException('srclib ' + name + ' not found.')
1048 srclib = metadata.srclibs[name]
1050 sdir = os.path.join(srclib_dir, name)
1053 vcs = getvcs(srclib["Repo Type"], srclib["Repo"], sdir)
1054 vcs.srclib = (name, number, sdir)
1056 vcs.gotorevision(ref)
1063 libdir = os.path.join(sdir, subdir)
1064 elif srclib["Subdir"]:
1065 for subdir in srclib["Subdir"]:
1066 libdir_candidate = os.path.join(sdir, subdir)
1067 if os.path.exists(libdir_candidate):
1068 libdir = libdir_candidate
1074 if srclib["Srclibs"]:
1076 for lib in srclib["Srclibs"].replace(';', ',').split(','):
1078 for t in srclibpaths:
1083 raise VCSException('Missing recursive srclib %s for %s' % (
1085 place_srclib(libdir, n, s_tuple[2])
1088 remove_signing_keys(sdir)
1089 remove_debuggable_flags(sdir)
1093 if srclib["Prepare"]:
1094 cmd = replace_config_vars(srclib["Prepare"])
1096 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=libdir)
1097 if p.returncode != 0:
1098 raise BuildException("Error running prepare command for srclib %s"
1104 return (name, number, libdir)
1107 # Prepare the source code for a particular build
1108 # 'vcs' - the appropriate vcs object for the application
1109 # 'app' - the application details from the metadata
1110 # 'build' - the build details from the metadata
1111 # 'build_dir' - the path to the build directory, usually
1113 # 'srclib_dir' - the path to the source libraries directory, usually
1115 # 'extlib_dir' - the path to the external libraries directory, usually
1117 # Returns the (root, srclibpaths) where:
1118 # 'root' is the root directory, which may be the same as 'build_dir' or may
1119 # be a subdirectory of it.
1120 # 'srclibpaths' is information on the srclibs being used
1121 def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=False):
1123 # Optionally, the actual app source can be in a subdirectory
1125 root_dir = os.path.join(build_dir, build['subdir'])
1127 root_dir = build_dir
1129 # Get a working copy of the right revision
1130 logging.info("Getting source for revision " + build['commit'])
1131 vcs.gotorevision(build['commit'])
1133 # Initialise submodules if requred
1134 if build['submodules']:
1135 logging.info("Initialising submodules")
1136 vcs.initsubmodules()
1138 # Check that a subdir (if we're using one) exists. This has to happen
1139 # after the checkout, since it might not exist elsewhere
1140 if not os.path.exists(root_dir):
1141 raise BuildException('Missing subdir ' + root_dir)
1143 # Run an init command if one is required
1145 cmd = replace_config_vars(build['init'])
1146 logging.info("Running 'init' commands in %s" % root_dir)
1148 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
1149 if p.returncode != 0:
1150 raise BuildException("Error running init command for %s:%s" %
1151 (app['id'], build['version']), p.output)
1153 # Apply patches if any
1155 logging.info("Applying patches")
1156 for patch in build['patch']:
1157 patch = patch.strip()
1158 logging.info("Applying " + patch)
1159 patch_path = os.path.join('metadata', app['id'], patch)
1160 p = FDroidPopen(['patch', '-p1', '-i', os.path.abspath(patch_path)], cwd=build_dir)
1161 if p.returncode != 0:
1162 raise BuildException("Failed to apply patch %s" % patch_path)
1164 # Get required source libraries
1166 if build['srclibs']:
1167 logging.info("Collecting source libraries")
1168 for lib in build['srclibs']:
1169 srclibpaths.append(getsrclib(lib, srclib_dir, srclibpaths,
1172 for name, number, libpath in srclibpaths:
1173 place_srclib(root_dir, int(number) if number else None, libpath)
1175 basesrclib = vcs.getsrclib()
1176 # If one was used for the main source, add that too.
1178 srclibpaths.append(basesrclib)
1180 # Update the local.properties file
1181 localprops = [os.path.join(build_dir, 'local.properties')]
1183 localprops += [os.path.join(root_dir, 'local.properties')]
1184 for path in localprops:
1185 if not os.path.isfile(path):
1187 logging.info("Updating properties file at %s" % path)
1192 # Fix old-fashioned 'sdk-location' by copying
1193 # from sdk.dir, if necessary
1194 if build['oldsdkloc']:
1195 sdkloc = re.match(r".*^sdk.dir=(\S+)$.*", props,
1196 re.S | re.M).group(1)
1197 props += "sdk-location=%s\n" % sdkloc
1199 props += "sdk.dir=%s\n" % config['sdk_path']
1200 props += "sdk-location=%s\n" % config['sdk_path']
1201 if 'ndk_path' in config:
1203 props += "ndk.dir=%s\n" % config['ndk_path']
1204 props += "ndk-location=%s\n" % config['ndk_path']
1205 # Add java.encoding if necessary
1206 if build['encoding']:
1207 props += "java.encoding=%s\n" % build['encoding']
1213 if build['type'] == 'gradle':
1214 flavours = build['gradle']
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, flavours):
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, flavours):
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()
1657 p = subprocess.Popen(commands, cwd=cwd, shell=shell, env=env,
1658 stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
1660 raise BuildException("OSError while trying to execute " + ' '.join(commands) + ': ' + str(e))
1662 stdout_queue = Queue.Queue()
1663 stdout_reader = AsynchronousFileReader(p.stdout, stdout_queue)
1664 stdout_reader.start()
1666 # Check the queue for output (until there is no more to get)
1667 while not stdout_reader.eof():
1668 while not stdout_queue.empty():
1669 line = stdout_queue.get()
1670 if output and options.verbose:
1671 # Output directly to console
1672 sys.stderr.write(line)
1674 result.output += line
1678 result.returncode = p.wait()
1682 def remove_signing_keys(build_dir):
1683 comment = re.compile(r'[ ]*//')
1684 signing_configs = re.compile(r'^[\t ]*signingConfigs[ \t]*{[ \t]*$')
1686 re.compile(r'^[\t ]*signingConfig [^ ]*$'),
1687 re.compile(r'.*android\.signingConfigs\.[^{]*$'),
1688 re.compile(r'.*variant\.outputFile = .*'),
1689 re.compile(r'.*\.readLine\(.*'),
1691 for root, dirs, files in os.walk(build_dir):
1692 if 'build.gradle' in files:
1693 path = os.path.join(root, 'build.gradle')
1695 with open(path, "r") as o:
1696 lines = o.readlines()
1701 with open(path, "w") as o:
1703 if comment.match(line):
1707 opened += line.count('{')
1708 opened -= line.count('}')
1711 if signing_configs.match(line):
1716 if any(s.match(line) for s in line_matches):
1724 logging.info("Cleaned build.gradle of keysigning configs at %s" % path)
1727 'project.properties',
1729 'default.properties',
1732 if propfile in files:
1733 path = os.path.join(root, propfile)
1735 with open(path, "r") as o:
1736 lines = o.readlines()
1740 with open(path, "w") as o:
1742 if any(line.startswith(s) for s in ('key.store', 'key.alias')):
1749 logging.info("Cleaned %s of keysigning configs at %s" % (propfile, path))
1752 def replace_config_vars(cmd):
1753 cmd = cmd.replace('$$SDK$$', config['sdk_path'])
1754 cmd = cmd.replace('$$NDK$$', config['ndk_path'])
1755 cmd = cmd.replace('$$MVN3$$', config['mvn3'])
1759 def place_srclib(root_dir, number, libpath):
1762 relpath = os.path.relpath(libpath, root_dir)
1763 proppath = os.path.join(root_dir, 'project.properties')
1766 if os.path.isfile(proppath):
1767 with open(proppath, "r") as o:
1768 lines = o.readlines()
1770 with open(proppath, "w") as o:
1773 if line.startswith('android.library.reference.%d=' % number):
1774 o.write('android.library.reference.%d=%s\n' % (number, relpath))
1779 o.write('android.library.reference.%d=%s\n' % (number, relpath))