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')]
837 for flavour in flavours:
838 possible_manifests.append(
839 os.path.join(app_dir, 'src', flavour, 'AndroidManifest.xml'))
841 return [path for path in possible_manifests if os.path.isfile(path)]
844 # Retrieve the package name. Returns the name, or None if not found.
845 def fetch_real_name(app_dir, flavours):
846 app_search = re.compile(r'.*<application.*').search
847 name_search = re.compile(r'.*android:label="([^"]+)".*').search
849 for f in manifest_paths(app_dir, flavours):
850 if not has_extension(f, 'xml'):
852 logging.debug("fetch_real_name: Checking manifest at " + f)
858 matches = name_search(line)
860 stringname = matches.group(1)
861 logging.debug("fetch_real_name: using string " + stringname)
862 result = retrieve_string(app_dir, stringname)
864 result = result.strip()
869 # Retrieve the version name
870 def version_name(original, app_dir, flavours):
871 for f in manifest_paths(app_dir, flavours):
872 if not has_extension(f, 'xml'):
874 string = retrieve_string(app_dir, original)
880 def get_library_references(root_dir):
882 proppath = os.path.join(root_dir, 'project.properties')
883 if not os.path.isfile(proppath):
885 with open(proppath) as f:
886 for line in f.readlines():
887 if not line.startswith('android.library.reference.'):
889 path = line.split('=')[1].strip()
890 relpath = os.path.join(root_dir, path)
891 if not os.path.isdir(relpath):
893 logging.debug("Found subproject at %s" % path)
894 libraries.append(path)
898 def ant_subprojects(root_dir):
899 subprojects = get_library_references(root_dir)
900 for subpath in subprojects:
901 subrelpath = os.path.join(root_dir, subpath)
902 for p in get_library_references(subrelpath):
903 relp = os.path.normpath(os.path.join(subpath, p))
904 if relp not in subprojects:
905 subprojects.insert(0, relp)
909 def remove_debuggable_flags(root_dir):
910 # Remove forced debuggable flags
911 logging.debug("Removing debuggable flags from %s" % root_dir)
912 for root, dirs, files in os.walk(root_dir):
913 if 'AndroidManifest.xml' in files:
914 path = os.path.join(root, 'AndroidManifest.xml')
915 p = SilentPopen(['sed', '-i', 's/android:debuggable="[^"]*"//g', path])
916 if p.returncode != 0:
917 raise BuildException("Failed to remove debuggable flags of %s" % path)
920 # Extract some information from the AndroidManifest.xml at the given path.
921 # Returns (version, vercode, package), any or all of which might be None.
922 # All values returned are strings.
923 def parse_androidmanifests(paths, ignoreversions=None):
926 return (None, None, None)
928 vcsearch = re.compile(r'.*:versionCode="([0-9]+?)".*').search
929 vnsearch = re.compile(r'.*:versionName="([^"]+?)".*').search
930 psearch = re.compile(r'.*package="([^"]+)".*').search
932 vcsearch_g = re.compile(r'.*versionCode *=* *["\']*([0-9]+)["\']*').search
933 vnsearch_g = re.compile(r'.*versionName *=* *(["\'])((?:(?=(\\?))\3.)*?)\1.*').search
934 psearch_g = re.compile(r'.*packageName *=* *["\']([^"]+)["\'].*').search
936 ignoresearch = re.compile(ignoreversions).search if ignoreversions else None
944 gradle = has_extension(path, 'gradle')
947 # Remember package name, may be defined separately from version+vercode
948 package = max_package
950 for line in file(path):
953 matches = psearch_g(line)
955 matches = psearch(line)
957 package = matches.group(1)
960 matches = vnsearch_g(line)
962 matches = vnsearch(line)
964 version = matches.group(2 if gradle else 1)
967 matches = vcsearch_g(line)
969 matches = vcsearch(line)
971 vercode = matches.group(1)
973 # Always grab the package name and version name in case they are not
974 # together with the highest version code
975 if max_package is None and package is not None:
976 max_package = package
977 if max_version is None and version is not None:
978 max_version = version
980 if max_vercode is None or (vercode is not None and vercode > max_vercode):
981 if not ignoresearch or not ignoresearch(version):
982 if version is not None:
983 max_version = version
984 if vercode is not None:
985 max_vercode = vercode
986 if package is not None:
987 max_package = package
989 max_version = "Ignore"
991 if max_version is None:
992 max_version = "Unknown"
994 return (max_version, max_vercode, max_package)
997 class FDroidException(Exception):
998 def __init__(self, value, detail=None):
1000 self.detail = detail
1002 def get_wikitext(self):
1003 ret = repr(self.value) + "\n"
1007 txt = self.detail[-8192:] if len(self.detail) > 8192 else self.detail
1015 ret += "\n==== detail begin ====\n%s\n==== detail end ====" % self.detail.strip()
1019 class VCSException(FDroidException):
1023 class BuildException(FDroidException):
1027 # Get the specified source library.
1028 # Returns the path to it. Normally this is the path to be used when referencing
1029 # it, which may be a subdirectory of the actual project. If you want the base
1030 # directory of the project, pass 'basepath=True'.
1031 def getsrclib(spec, srclib_dir, srclibpaths=[], subdir=None,
1032 basepath=False, raw=False, prepare=True, preponly=False):
1040 name, ref = spec.split('@')
1042 number, name = name.split(':', 1)
1044 name, subdir = name.split('/', 1)
1046 if name not in metadata.srclibs:
1047 raise VCSException('srclib ' + name + ' not found.')
1049 srclib = metadata.srclibs[name]
1051 sdir = os.path.join(srclib_dir, name)
1054 vcs = getvcs(srclib["Repo Type"], srclib["Repo"], sdir)
1055 vcs.srclib = (name, number, sdir)
1057 vcs.gotorevision(ref)
1064 libdir = os.path.join(sdir, subdir)
1065 elif srclib["Subdir"]:
1066 for subdir in srclib["Subdir"]:
1067 libdir_candidate = os.path.join(sdir, subdir)
1068 if os.path.exists(libdir_candidate):
1069 libdir = libdir_candidate
1075 if srclib["Srclibs"]:
1077 for lib in srclib["Srclibs"].replace(';', ',').split(','):
1079 for t in srclibpaths:
1084 raise VCSException('Missing recursive srclib %s for %s' % (
1086 place_srclib(libdir, n, s_tuple[2])
1089 remove_signing_keys(sdir)
1090 remove_debuggable_flags(sdir)
1094 if srclib["Prepare"]:
1095 cmd = replace_config_vars(srclib["Prepare"])
1097 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=libdir)
1098 if p.returncode != 0:
1099 raise BuildException("Error running prepare command for srclib %s"
1105 return (name, number, libdir)
1108 # Prepare the source code for a particular build
1109 # 'vcs' - the appropriate vcs object for the application
1110 # 'app' - the application details from the metadata
1111 # 'build' - the build details from the metadata
1112 # 'build_dir' - the path to the build directory, usually
1114 # 'srclib_dir' - the path to the source libraries directory, usually
1116 # 'extlib_dir' - the path to the external libraries directory, usually
1118 # Returns the (root, srclibpaths) where:
1119 # 'root' is the root directory, which may be the same as 'build_dir' or may
1120 # be a subdirectory of it.
1121 # 'srclibpaths' is information on the srclibs being used
1122 def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=False):
1124 # Optionally, the actual app source can be in a subdirectory
1126 root_dir = os.path.join(build_dir, build['subdir'])
1128 root_dir = build_dir
1130 # Get a working copy of the right revision
1131 logging.info("Getting source for revision " + build['commit'])
1132 vcs.gotorevision(build['commit'])
1134 # Initialise submodules if requred
1135 if build['submodules']:
1136 logging.info("Initialising submodules")
1137 vcs.initsubmodules()
1139 # Check that a subdir (if we're using one) exists. This has to happen
1140 # after the checkout, since it might not exist elsewhere
1141 if not os.path.exists(root_dir):
1142 raise BuildException('Missing subdir ' + root_dir)
1144 # Run an init command if one is required
1146 cmd = replace_config_vars(build['init'])
1147 logging.info("Running 'init' commands in %s" % root_dir)
1149 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
1150 if p.returncode != 0:
1151 raise BuildException("Error running init command for %s:%s" %
1152 (app['id'], build['version']), p.output)
1154 # Apply patches if any
1156 logging.info("Applying patches")
1157 for patch in build['patch']:
1158 patch = patch.strip()
1159 logging.info("Applying " + patch)
1160 patch_path = os.path.join('metadata', app['id'], patch)
1161 p = FDroidPopen(['patch', '-p1', '-i', os.path.abspath(patch_path)], cwd=build_dir)
1162 if p.returncode != 0:
1163 raise BuildException("Failed to apply patch %s" % patch_path)
1165 # Get required source libraries
1167 if build['srclibs']:
1168 logging.info("Collecting source libraries")
1169 for lib in build['srclibs']:
1170 srclibpaths.append(getsrclib(lib, srclib_dir, srclibpaths,
1173 for name, number, libpath in srclibpaths:
1174 place_srclib(root_dir, int(number) if number else None, libpath)
1176 basesrclib = vcs.getsrclib()
1177 # If one was used for the main source, add that too.
1179 srclibpaths.append(basesrclib)
1181 # Update the local.properties file
1182 localprops = [os.path.join(build_dir, 'local.properties')]
1184 localprops += [os.path.join(root_dir, 'local.properties')]
1185 for path in localprops:
1186 if not os.path.isfile(path):
1188 logging.info("Updating properties file at %s" % path)
1193 # Fix old-fashioned 'sdk-location' by copying
1194 # from sdk.dir, if necessary
1195 if build['oldsdkloc']:
1196 sdkloc = re.match(r".*^sdk.dir=(\S+)$.*", props,
1197 re.S | re.M).group(1)
1198 props += "sdk-location=%s\n" % sdkloc
1200 props += "sdk.dir=%s\n" % config['sdk_path']
1201 props += "sdk-location=%s\n" % config['sdk_path']
1202 if 'ndk_path' in config:
1204 props += "ndk.dir=%s\n" % config['ndk_path']
1205 props += "ndk-location=%s\n" % config['ndk_path']
1206 # Add java.encoding if necessary
1207 if build['encoding']:
1208 props += "java.encoding=%s\n" % build['encoding']
1214 if build['type'] == 'gradle':
1215 flavours = build['gradle']
1216 if len(flavours) == 1 and flavours[0] in ['main', 'yes', '']:
1219 version_regex = re.compile(r".*'com\.android\.tools\.build:gradle:([^\.]+\.[^\.]+).*'.*")
1220 gradlepluginver = None
1222 gradle_files = [os.path.join(root_dir, 'build.gradle')]
1224 # Parent dir build.gradle
1225 parent_dir = os.path.normpath(os.path.join(root_dir, '..'))
1226 if parent_dir.startswith(build_dir):
1227 gradle_files.append(os.path.join(parent_dir, 'build.gradle'))
1229 for path in gradle_files:
1232 if not os.path.isfile(path):
1234 with open(path) as f:
1236 match = version_regex.match(line)
1238 gradlepluginver = match.group(1)
1242 build['gradlepluginver'] = LooseVersion(gradlepluginver)
1244 logging.warn("Could not fetch the gradle plugin version, defaulting to 0.11")
1245 build['gradlepluginver'] = LooseVersion('0.11')
1248 n = build["target"].split('-')[1]
1249 SilentPopen(['sed', '-i',
1250 's@compileSdkVersion *[0-9]*@compileSdkVersion ' + n + '@g',
1254 # Remove forced debuggable flags
1255 remove_debuggable_flags(root_dir)
1257 # Insert version code and number into the manifest if necessary
1258 if build['forceversion']:
1259 logging.info("Changing the version name")
1260 for path in manifest_paths(root_dir, flavours):
1261 if not os.path.isfile(path):
1263 if has_extension(path, 'xml'):
1264 p = SilentPopen(['sed', '-i',
1265 's/android:versionName="[^"]*"/android:versionName="'
1266 + build['version'] + '"/g',
1268 if p.returncode != 0:
1269 raise BuildException("Failed to amend manifest")
1270 elif has_extension(path, 'gradle'):
1271 p = SilentPopen(['sed', '-i',
1272 's/versionName *=* *"[^"]*"/versionName = "'
1273 + build['version'] + '"/g',
1275 if p.returncode != 0:
1276 raise BuildException("Failed to amend build.gradle")
1277 if build['forcevercode']:
1278 logging.info("Changing the version code")
1279 for path in manifest_paths(root_dir, flavours):
1280 if not os.path.isfile(path):
1282 if has_extension(path, 'xml'):
1283 p = SilentPopen(['sed', '-i',
1284 's/android:versionCode="[^"]*"/android:versionCode="'
1285 + build['vercode'] + '"/g',
1287 if p.returncode != 0:
1288 raise BuildException("Failed to amend manifest")
1289 elif has_extension(path, 'gradle'):
1290 p = SilentPopen(['sed', '-i',
1291 's/versionCode *=* *[0-9]*/versionCode = '
1292 + build['vercode'] + '/g',
1294 if p.returncode != 0:
1295 raise BuildException("Failed to amend build.gradle")
1297 # Delete unwanted files
1299 logging.info("Removing specified files")
1300 for part in getpaths(build_dir, build, 'rm'):
1301 dest = os.path.join(build_dir, part)
1302 logging.info("Removing {0}".format(part))
1303 if os.path.lexists(dest):
1304 if os.path.islink(dest):
1305 SilentPopen(['unlink ' + dest], shell=True)
1307 SilentPopen(['rm -rf ' + dest], shell=True)
1309 logging.info("...but it didn't exist")
1311 remove_signing_keys(build_dir)
1313 # Add required external libraries
1314 if build['extlibs']:
1315 logging.info("Collecting prebuilt libraries")
1316 libsdir = os.path.join(root_dir, 'libs')
1317 if not os.path.exists(libsdir):
1319 for lib in build['extlibs']:
1321 logging.info("...installing extlib {0}".format(lib))
1322 libf = os.path.basename(lib)
1323 libsrc = os.path.join(extlib_dir, lib)
1324 if not os.path.exists(libsrc):
1325 raise BuildException("Missing extlib file {0}".format(libsrc))
1326 shutil.copyfile(libsrc, os.path.join(libsdir, libf))
1328 # Run a pre-build command if one is required
1329 if build['prebuild']:
1330 logging.info("Running 'prebuild' commands in %s" % root_dir)
1332 cmd = replace_config_vars(build['prebuild'])
1334 # Substitute source library paths into prebuild commands
1335 for name, number, libpath in srclibpaths:
1336 libpath = os.path.relpath(libpath, root_dir)
1337 cmd = cmd.replace('$$' + name + '$$', libpath)
1339 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
1340 if p.returncode != 0:
1341 raise BuildException("Error running prebuild command for %s:%s" %
1342 (app['id'], build['version']), p.output)
1344 # Generate (or update) the ant build file, build.xml...
1345 if build['update'] and build['update'] != ['no'] and build['type'] == 'ant':
1346 parms = [config['android'], 'update', 'lib-project']
1347 lparms = [config['android'], 'update', 'project']
1350 parms += ['-t', build['target']]
1351 lparms += ['-t', build['target']]
1352 if build['update'] == ['auto']:
1353 update_dirs = ant_subprojects(root_dir) + ['.']
1355 update_dirs = build['update']
1357 for d in update_dirs:
1358 subdir = os.path.join(root_dir, d)
1360 logging.debug("Updating main project")
1361 cmd = parms + ['-p', d]
1363 logging.debug("Updating subproject %s" % d)
1364 cmd = lparms + ['-p', d]
1365 p = FDroidPopen(cmd, cwd=root_dir)
1366 # Check to see whether an error was returned without a proper exit
1367 # code (this is the case for the 'no target set or target invalid'
1369 if p.returncode != 0 or p.output.startswith("Error: "):
1370 raise BuildException("Failed to update project at %s" % d, p.output)
1371 # Clean update dirs via ant
1373 logging.info("Cleaning subproject %s" % d)
1374 p = FDroidPopen(['ant', 'clean'], cwd=subdir)
1376 return (root_dir, srclibpaths)
1379 # Split and extend via globbing the paths from a field
1380 def getpaths(build_dir, build, field):
1382 for p in build[field]:
1384 full_path = os.path.join(build_dir, p)
1385 full_path = os.path.normpath(full_path)
1386 paths += [r[len(build_dir) + 1:] for r in glob.glob(full_path)]
1390 # Scan the source code in the given directory (and all subdirectories)
1391 # and return the number of fatal problems encountered
1392 def scan_source(build_dir, root_dir, thisbuild):
1396 # Common known non-free blobs (always lower case):
1398 re.compile(r'flurryagent', re.IGNORECASE),
1399 re.compile(r'paypal.*mpl', re.IGNORECASE),
1400 re.compile(r'google.*analytics', re.IGNORECASE),
1401 re.compile(r'admob.*sdk.*android', re.IGNORECASE),
1402 re.compile(r'google.*ad.*view', re.IGNORECASE),
1403 re.compile(r'google.*admob', re.IGNORECASE),
1404 re.compile(r'google.*play.*services', re.IGNORECASE),
1405 re.compile(r'crittercism', re.IGNORECASE),
1406 re.compile(r'heyzap', re.IGNORECASE),
1407 re.compile(r'jpct.*ae', re.IGNORECASE),
1408 re.compile(r'youtube.*android.*player.*api', re.IGNORECASE),
1409 re.compile(r'bugsense', re.IGNORECASE),
1410 re.compile(r'crashlytics', re.IGNORECASE),
1411 re.compile(r'ouya.*sdk', re.IGNORECASE),
1412 re.compile(r'libspen23', re.IGNORECASE),
1415 scanignore = getpaths(build_dir, thisbuild, 'scanignore')
1416 scandelete = getpaths(build_dir, thisbuild, 'scandelete')
1419 ms = magic.open(magic.MIME_TYPE)
1421 except AttributeError:
1425 for i in scanignore:
1426 if fd.startswith(i):
1431 for i in scandelete:
1432 if fd.startswith(i):
1436 def removeproblem(what, fd, fp):
1437 logging.info('Removing %s at %s' % (what, fd))
1440 def warnproblem(what, fd):
1441 logging.warn('Found %s at %s' % (what, fd))
1443 def handleproblem(what, fd, fp):
1445 logging.info('Ignoring %s at %s' % (what, fd))
1447 removeproblem(what, fd, fp)
1449 logging.error('Found %s at %s' % (what, fd))
1453 # Iterate through all files in the source code
1454 for r, d, f in os.walk(build_dir, topdown=True):
1456 # It's topdown, so checking the basename is enough
1457 for ignoredir in ('.hg', '.git', '.svn', '.bzr'):
1463 # Path (relative) to the file
1464 fp = os.path.join(r, curfile)
1465 fd = fp[len(build_dir) + 1:]
1468 mime = magic.from_file(fp, mime=True) if ms is None else ms.file(fp)
1469 except UnicodeError:
1470 warnproblem('malformed magic number', fd)
1472 if mime == 'application/x-sharedlib':
1473 count += handleproblem('shared library', fd, fp)
1475 elif mime == 'application/x-archive':
1476 count += handleproblem('static library', fd, fp)
1478 elif mime == 'application/x-executable':
1479 count += handleproblem('binary executable', fd, fp)
1481 elif mime == 'application/x-java-applet':
1482 count += handleproblem('Java compiled class', fd, fp)
1487 'application/java-archive',
1488 'application/octet-stream',
1492 if has_extension(fp, 'apk'):
1493 removeproblem('APK file', fd, fp)
1495 elif has_extension(fp, 'jar'):
1497 if any(suspect.match(curfile) for suspect in usual_suspects):
1498 count += handleproblem('usual supect', fd, fp)
1500 warnproblem('JAR file', fd)
1502 elif has_extension(fp, 'zip'):
1503 warnproblem('ZIP file', fd)
1506 warnproblem('unknown compressed or binary file', fd)
1508 elif has_extension(fp, 'java'):
1509 for line in file(fp):
1510 if 'DexClassLoader' in line:
1511 count += handleproblem('DexClassLoader', fd, fp)
1516 # Presence of a jni directory without buildjni=yes might
1517 # indicate a problem (if it's not a problem, explicitly use
1518 # buildjni=no to bypass this check)
1519 if (os.path.exists(os.path.join(root_dir, 'jni')) and
1520 not thisbuild['buildjni']):
1521 logging.error('Found jni directory, but buildjni is not enabled. Set it to \'no\' to ignore.')
1530 self.path = os.path.join('stats', 'known_apks.txt')
1532 if os.path.exists(self.path):
1533 for line in file(self.path):
1534 t = line.rstrip().split(' ')
1536 self.apks[t[0]] = (t[1], None)
1538 self.apks[t[0]] = (t[1], time.strptime(t[2], '%Y-%m-%d'))
1539 self.changed = False
1541 def writeifchanged(self):
1543 if not os.path.exists('stats'):
1545 f = open(self.path, 'w')
1547 for apk, app in self.apks.iteritems():
1549 line = apk + ' ' + appid
1551 line += ' ' + time.strftime('%Y-%m-%d', added)
1553 for line in sorted(lst):
1554 f.write(line + '\n')
1557 # Record an apk (if it's new, otherwise does nothing)
1558 # Returns the date it was added.
1559 def recordapk(self, apk, app):
1560 if apk not in self.apks:
1561 self.apks[apk] = (app, time.gmtime(time.time()))
1563 _, added = self.apks[apk]
1566 # Look up information - given the 'apkname', returns (app id, date added/None).
1567 # Or returns None for an unknown apk.
1568 def getapp(self, apkname):
1569 if apkname in self.apks:
1570 return self.apks[apkname]
1573 # Get the most recent 'num' apps added to the repo, as a list of package ids
1574 # with the most recent first.
1575 def getlatest(self, num):
1577 for apk, app in self.apks.iteritems():
1581 if apps[appid] > added:
1585 sortedapps = sorted(apps.iteritems(), key=operator.itemgetter(1))[-num:]
1586 lst = [app for app, _ in sortedapps]
1591 def isApkDebuggable(apkfile, config):
1592 """Returns True if the given apk file is debuggable
1594 :param apkfile: full path to the apk to check"""
1596 p = SilentPopen([os.path.join(config['sdk_path'], 'build-tools',
1597 config['build_tools'], 'aapt'),
1598 'dump', 'xmltree', apkfile, 'AndroidManifest.xml'])
1599 if p.returncode != 0:
1600 logging.critical("Failed to get apk manifest information")
1602 for line in p.output.splitlines():
1603 if 'android:debuggable' in line and not line.endswith('0x0'):
1608 class AsynchronousFileReader(threading.Thread):
1610 Helper class to implement asynchronous reading of a file
1611 in a separate thread. Pushes read lines on a queue to
1612 be consumed in another thread.
1615 def __init__(self, fd, queue):
1616 assert isinstance(queue, Queue.Queue)
1617 assert callable(fd.readline)
1618 threading.Thread.__init__(self)
1623 '''The body of the tread: read lines and put them on the queue.'''
1624 for line in iter(self._fd.readline, ''):
1625 self._queue.put(line)
1628 '''Check whether there is no more content to expect.'''
1629 return not self.is_alive() and self._queue.empty()
1637 def SilentPopen(commands, cwd=None, shell=False):
1638 return FDroidPopen(commands, cwd=cwd, shell=shell, output=False)
1641 def FDroidPopen(commands, cwd=None, shell=False, output=True):
1643 Run a command and capture the possibly huge output.
1645 :param commands: command and argument list like in subprocess.Popen
1646 :param cwd: optionally specifies a working directory
1647 :returns: A PopenResult.
1653 cwd = os.path.normpath(cwd)
1654 logging.debug("Directory: %s" % cwd)
1655 logging.debug("> %s" % ' '.join(commands))
1657 result = PopenResult()
1660 p = subprocess.Popen(commands, cwd=cwd, shell=shell, env=env,
1661 stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
1663 raise BuildException("OSError while trying to execute " + ' '.join(commands) + ': ' + str(e))
1665 stdout_queue = Queue.Queue()
1666 stdout_reader = AsynchronousFileReader(p.stdout, stdout_queue)
1667 stdout_reader.start()
1669 # Check the queue for output (until there is no more to get)
1670 while not stdout_reader.eof():
1671 while not stdout_queue.empty():
1672 line = stdout_queue.get()
1673 if output and options.verbose:
1674 # Output directly to console
1675 sys.stderr.write(line)
1677 result.output += line
1681 result.returncode = p.wait()
1685 def remove_signing_keys(build_dir):
1686 comment = re.compile(r'[ ]*//')
1687 signing_configs = re.compile(r'^[\t ]*signingConfigs[ \t]*{[ \t]*$')
1689 re.compile(r'^[\t ]*signingConfig [^ ]*$'),
1690 re.compile(r'.*android\.signingConfigs\.[^{]*$'),
1691 re.compile(r'.*variant\.outputFile = .*'),
1692 re.compile(r'.*\.readLine\(.*'),
1694 for root, dirs, files in os.walk(build_dir):
1695 if 'build.gradle' in files:
1696 path = os.path.join(root, 'build.gradle')
1698 with open(path, "r") as o:
1699 lines = o.readlines()
1704 with open(path, "w") as o:
1706 if comment.match(line):
1710 opened += line.count('{')
1711 opened -= line.count('}')
1714 if signing_configs.match(line):
1719 if any(s.match(line) for s in line_matches):
1727 logging.info("Cleaned build.gradle of keysigning configs at %s" % path)
1730 'project.properties',
1732 'default.properties',
1735 if propfile in files:
1736 path = os.path.join(root, propfile)
1738 with open(path, "r") as o:
1739 lines = o.readlines()
1743 with open(path, "w") as o:
1745 if any(line.startswith(s) for s in ('key.store', 'key.alias')):
1752 logging.info("Cleaned %s of keysigning configs at %s" % (propfile, path))
1755 def replace_config_vars(cmd):
1756 cmd = cmd.replace('$$SDK$$', config['sdk_path'])
1757 cmd = cmd.replace('$$NDK$$', config['ndk_path'])
1758 cmd = cmd.replace('$$MVN3$$', config['mvn3'])
1762 def place_srclib(root_dir, number, libpath):
1765 relpath = os.path.relpath(libpath, root_dir)
1766 proppath = os.path.join(root_dir, 'project.properties')
1769 if os.path.isfile(proppath):
1770 with open(proppath, "r") as o:
1771 lines = o.readlines()
1773 with open(proppath, "w") as o:
1776 if line.startswith('android.library.reference.%d=' % number):
1777 o.write('android.library.reference.%d=%s\n' % (number, relpath))
1782 o.write('android.library.reference.%d=%s\n' % (number, relpath))