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
43 'sdk_path': "$ANDROID_HOME",
44 'ndk_path': "$ANDROID_NDK",
45 'build_tools': "20.0.0",
49 'sync_from_local_copy_dir': False,
50 'update_stats': False,
54 'stats_to_carbon': False,
56 'build_server_always': False,
57 'keystore': os.path.join("$HOME", '.local', 'share', 'fdroidserver', 'keystore.jks'),
58 'smartcardoptions': [],
64 'repo_url': "https://MyFirstFDroidRepo.org/fdroid/repo",
65 'repo_name': "My First FDroid Repo Demo",
66 'repo_icon': "fdroid-icon.png",
67 'repo_description': '''
68 This is a repository of apps to be used with FDroid. Applications in this
69 repository are either official binaries built by the original application
70 developers, or are binaries built from source by the admin of f-droid.org
71 using the tools on https://gitlab.com/u/fdroid.
77 def fill_config_defaults(config):
78 for k, v in default_config.items():
82 # Expand paths (~users and $vars)
83 for k in ['sdk_path', 'ndk_path', 'ant', 'mvn3', 'gradle', 'keystore', 'repo_icon']:
86 v = os.path.expanduser(v)
87 v = os.path.expandvars(v)
90 config[k + '_orig'] = orig
93 def read_config(opts, config_file='config.py'):
94 """Read the repository config
96 The config is read from config_file, which is in the current directory when
97 any of the repo management commands are used.
99 global config, options, env
101 if config is not None:
103 if not os.path.isfile(config_file):
104 logging.critical("Missing config file - is this a repo directory?")
111 logging.debug("Reading %s" % config_file)
112 execfile(config_file, config)
114 # smartcardoptions must be a list since its command line args for Popen
115 if 'smartcardoptions' in config:
116 config['smartcardoptions'] = config['smartcardoptions'].split(' ')
117 elif 'keystore' in config and config['keystore'] == 'NONE':
118 # keystore='NONE' means use smartcard, these are required defaults
119 config['smartcardoptions'] = ['-storetype', 'PKCS11', '-providerName',
120 'SunPKCS11-OpenSC', '-providerClass',
121 'sun.security.pkcs11.SunPKCS11',
122 '-providerArg', 'opensc-fdroid.cfg']
124 if any(k in config for k in ["keystore", "keystorepass", "keypass"]):
125 st = os.stat(config_file)
126 if st.st_mode & stat.S_IRWXG or st.st_mode & stat.S_IRWXO:
127 logging.warn("unsafe permissions on {0} (should be 0600)!".format(config_file))
129 fill_config_defaults(config)
131 if not test_sdk_exists(config):
134 if not test_build_tools_exists(config):
139 os.path.join(config['sdk_path'], 'build-tools', config['build_tools'], 'aapt'),
142 os.path.join(config['sdk_path'], 'tools', 'zipalign'),
143 os.path.join(config['sdk_path'], 'build-tools', config['build_tools'], 'zipalign'),
146 os.path.join(config['sdk_path'], 'tools', 'android'),
149 os.path.join(config['sdk_path'], 'platform-tools', 'adb'),
153 for b, paths in bin_paths.items():
156 if os.path.isfile(path):
159 if config[b] is None:
160 logging.warn("Could not find %s in any of the following paths:\n%s" % (
161 b, '\n'.join(paths)))
163 # There is no standard, so just set up the most common environment
166 for n in ['ANDROID_HOME', 'ANDROID_SDK']:
167 env[n] = config['sdk_path']
168 for n in ['ANDROID_NDK', 'NDK']:
169 env[n] = config['ndk_path']
171 for k in ["keystorepass", "keypass"]:
173 write_password_file(k)
175 for k in ["repo_description", "archive_description"]:
177 config[k] = clean_description(config[k])
179 if 'serverwebroot' in config:
180 if isinstance(config['serverwebroot'], basestring):
181 roots = [config['serverwebroot']]
182 elif all(isinstance(item, basestring) for item in config['serverwebroot']):
183 roots = config['serverwebroot']
185 raise TypeError('only accepts strings, lists, and tuples')
187 for rootstr in roots:
188 # since this is used with rsync, where trailing slashes have
189 # meaning, ensure there is always a trailing slash
190 if rootstr[-1] != '/':
192 rootlist.append(rootstr.replace('//', '/'))
193 config['serverwebroot'] = rootlist
198 def test_sdk_exists(c):
199 if c['sdk_path'] is None:
200 # c['sdk_path'] is set to the value of ANDROID_HOME by default
201 logging.error('No Android SDK found!')
202 logging.error('You can use ANDROID_HOME to set the path to your SDK, i.e.:')
203 logging.error('\texport ANDROID_HOME=/opt/android-sdk')
205 if not os.path.exists(c['sdk_path']):
206 logging.critical('Android SDK path "' + c['sdk_path'] + '" does not exist!')
208 if not os.path.isdir(c['sdk_path']):
209 logging.critical('Android SDK path "' + c['sdk_path'] + '" is not a directory!')
211 for d in ['build-tools', 'platform-tools', 'tools']:
212 if not os.path.isdir(os.path.join(c['sdk_path'], d)):
213 logging.critical('Android SDK path "%s" does not contain "%s/"!' % (
219 def test_build_tools_exists(c):
220 if not test_sdk_exists(c):
222 build_tools = os.path.join(c['sdk_path'], 'build-tools')
223 versioned_build_tools = os.path.join(build_tools, c['build_tools'])
224 if not os.path.isdir(versioned_build_tools):
225 logging.critical('Android Build Tools path "'
226 + versioned_build_tools + '" does not exist!')
231 def write_password_file(pwtype, password=None):
233 writes out passwords to a protected file instead of passing passwords as
234 command line argments
236 filename = '.fdroid.' + pwtype + '.txt'
237 fd = os.open(filename, os.O_CREAT | os.O_TRUNC | os.O_WRONLY, 0600)
239 os.write(fd, config[pwtype])
241 os.write(fd, password)
243 config[pwtype + 'file'] = filename
246 # Given the arguments in the form of multiple appid:[vc] strings, this returns
247 # a dictionary with the set of vercodes specified for each package.
248 def read_pkg_args(args, allow_vercodes=False):
255 if allow_vercodes and ':' in p:
256 package, vercode = p.split(':')
258 package, vercode = p, None
259 if package not in vercodes:
260 vercodes[package] = [vercode] if vercode else []
262 elif vercode and vercode not in vercodes[package]:
263 vercodes[package] += [vercode] if vercode else []
268 # On top of what read_pkg_args does, this returns the whole app metadata, but
269 # limiting the builds list to the builds matching the vercodes specified.
270 def read_app_args(args, allapps, allow_vercodes=False):
272 vercodes = read_pkg_args(args, allow_vercodes)
278 for appid, app in allapps.iteritems():
279 if appid in vercodes:
282 if len(apps) != len(vercodes):
285 logging.critical("No such package: %s" % p)
286 raise FDroidException("Found invalid app ids in arguments")
288 raise FDroidException("No packages specified")
291 for appid, app in apps.iteritems():
295 app['builds'] = [b for b in app['builds'] if b['vercode'] in vc]
296 if len(app['builds']) != len(vercodes[appid]):
298 allvcs = [b['vercode'] for b in app['builds']]
299 for v in vercodes[appid]:
301 logging.critical("No such vercode %s for app %s" % (v, appid))
304 raise FDroidException("Found invalid vercodes for some apps")
309 def has_extension(filename, extension):
310 name, ext = os.path.splitext(filename)
311 ext = ext.lower()[1:]
312 return ext == extension
317 def clean_description(description):
318 'Remove unneeded newlines and spaces from a block of description text'
320 # this is split up by paragraph to make removing the newlines easier
321 for paragraph in re.split(r'\n\n', description):
322 paragraph = re.sub('\r', '', paragraph)
323 paragraph = re.sub('\n', ' ', paragraph)
324 paragraph = re.sub(' {2,}', ' ', paragraph)
325 paragraph = re.sub('^\s*(\w)', r'\1', paragraph)
326 returnstring += paragraph + '\n\n'
327 return returnstring.rstrip('\n')
330 def apknameinfo(filename):
332 filename = os.path.basename(filename)
333 if apk_regex is None:
334 apk_regex = re.compile(r"^(.+)_([0-9]+)\.apk$")
335 m = apk_regex.match(filename)
337 result = (m.group(1), m.group(2))
338 except AttributeError:
339 raise FDroidException("Invalid apk name: %s" % filename)
343 def getapkname(app, build):
344 return "%s_%s.apk" % (app['id'], build['vercode'])
347 def getsrcname(app, build):
348 return "%s_%s_src.tar.gz" % (app['id'], build['vercode'])
355 return app['Auto Name']
360 return '%s (%s)' % (app['Current Version'], app['Current Version Code'])
363 def getvcs(vcstype, remote, local):
365 return vcs_git(remote, local)
366 if vcstype == 'git-svn':
367 return vcs_gitsvn(remote, local)
369 return vcs_hg(remote, local)
371 return vcs_bzr(remote, local)
372 if vcstype == 'srclib':
373 if local != os.path.join('build', 'srclib', remote):
374 raise VCSException("Error: srclib paths are hard-coded!")
375 return getsrclib(remote, os.path.join('build', 'srclib'), raw=True)
377 raise VCSException("Deprecated vcs type 'svn' - please use 'git-svn' instead")
378 raise VCSException("Invalid vcs type " + vcstype)
381 def getsrclibvcs(name):
382 if name not in metadata.srclibs:
383 raise VCSException("Missing srclib " + name)
384 return metadata.srclibs[name]['Repo Type']
388 def __init__(self, remote, local):
390 # svn, git-svn and bzr may require auth
392 if self.repotype() in ('git-svn', 'bzr'):
394 self.username, remote = remote.split('@')
395 if ':' not in self.username:
396 raise VCSException("Password required with username")
397 self.username, self.password = self.username.split(':')
401 self.clone_failed = False
402 self.refreshed = False
408 # Take the local repository to a clean version of the given revision, which
409 # is specificed in the VCS's native format. Beforehand, the repository can
410 # be dirty, or even non-existent. If the repository does already exist
411 # locally, it will be updated from the origin, but only once in the
412 # lifetime of the vcs object.
413 # None is acceptable for 'rev' if you know you are cloning a clean copy of
414 # the repo - otherwise it must specify a valid revision.
415 def gotorevision(self, rev):
417 if self.clone_failed:
418 raise VCSException("Downloading the repository already failed once, not trying again.")
420 # The .fdroidvcs-id file for a repo tells us what VCS type
421 # and remote that directory was created from, allowing us to drop it
422 # automatically if either of those things changes.
423 fdpath = os.path.join(self.local, '..',
424 '.fdroidvcs-' + os.path.basename(self.local))
425 cdata = self.repotype() + ' ' + self.remote
428 if os.path.exists(self.local):
429 if os.path.exists(fdpath):
430 with open(fdpath, 'r') as f:
431 fsdata = f.read().strip()
437 "Repository details for %s changed - deleting" % (
441 logging.info("Repository details for %s missing - deleting" % (
444 shutil.rmtree(self.local)
449 self.gotorevisionx(rev)
450 except FDroidException, e:
453 # If necessary, write the .fdroidvcs file.
454 if writeback and not self.clone_failed:
455 with open(fdpath, 'w') as f:
461 # Derived classes need to implement this. It's called once basic checking
462 # has been performend.
463 def gotorevisionx(self, rev):
464 raise VCSException("This VCS type doesn't define gotorevisionx")
466 # Initialise and update submodules
467 def initsubmodules(self):
468 raise VCSException('Submodules not supported for this vcs type')
470 # Get a list of all known tags
472 raise VCSException('gettags not supported for this vcs type')
474 # Get a list of latest number tags
475 def latesttags(self, number):
476 raise VCSException('latesttags not supported for this vcs type')
478 # Get current commit reference (hash, revision, etc)
480 raise VCSException('getref not supported for this vcs type')
482 # Returns the srclib (name, path) used in setting up the current
493 # If the local directory exists, but is somehow not a git repository, git
494 # will traverse up the directory tree until it finds one that is (i.e.
495 # fdroidserver) and then we'll proceed to destroy it! This is called as
498 p = SilentPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local)
499 result = p.output.rstrip()
500 if not result.endswith(self.local):
501 raise VCSException('Repository mismatch')
503 def gotorevisionx(self, rev):
504 if not os.path.exists(self.local):
506 p = FDroidPopen(['git', 'clone', self.remote, self.local])
507 if p.returncode != 0:
508 self.clone_failed = True
509 raise VCSException("Git clone failed", p.output)
513 # Discard any working tree changes
514 p = SilentPopen(['git', 'reset', '--hard'], cwd=self.local)
515 if p.returncode != 0:
516 raise VCSException("Git reset failed", p.output)
517 # Remove untracked files now, in case they're tracked in the target
518 # revision (it happens!)
519 p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
520 if p.returncode != 0:
521 raise VCSException("Git clean failed", p.output)
522 if not self.refreshed:
523 # Get latest commits and tags from remote
524 p = FDroidPopen(['git', 'fetch', 'origin'], cwd=self.local)
525 if p.returncode != 0:
526 raise VCSException("Git fetch failed", p.output)
527 p = SilentPopen(['git', 'fetch', '--prune', '--tags', 'origin'], cwd=self.local)
528 if p.returncode != 0:
529 raise VCSException("Git fetch failed", p.output)
530 # Recreate origin/HEAD as git clone would do it, in case it disappeared
531 p = SilentPopen(['git', 'remote', 'set-head', 'origin', '--auto'], cwd=self.local)
532 if p.returncode != 0:
533 lines = p.output.splitlines()
534 if 'Multiple remote HEAD branches' not in lines[0]:
535 raise VCSException("Git remote set-head failed", p.output)
536 branch = lines[1].split(' ')[-1]
537 p2 = SilentPopen(['git', 'remote', 'set-head', 'origin', branch], cwd=self.local)
538 if p2.returncode != 0:
539 raise VCSException("Git remote set-head failed", p.output + '\n' + p2.output)
540 self.refreshed = True
541 # origin/HEAD is the HEAD of the remote, e.g. the "default branch" on
542 # a github repo. Most of the time this is the same as origin/master.
543 rev = rev or 'origin/HEAD'
544 p = SilentPopen(['git', 'checkout', '-f', rev], cwd=self.local)
545 if p.returncode != 0:
546 raise VCSException("Git checkout of '%s' failed" % rev, p.output)
547 # Get rid of any uncontrolled files left behind
548 p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
549 if p.returncode != 0:
550 raise VCSException("Git clean failed", p.output)
552 def initsubmodules(self):
554 submfile = os.path.join(self.local, '.gitmodules')
555 if not os.path.isfile(submfile):
556 raise VCSException("No git submodules available")
558 # fix submodules not accessible without an account and public key auth
559 with open(submfile, 'r') as f:
560 lines = f.readlines()
561 with open(submfile, 'w') as f:
563 if 'git@github.com' in line:
564 line = line.replace('git@github.com:', 'https://github.com/')
568 ['git', 'reset', '--hard'],
569 ['git', 'clean', '-dffx'],
571 p = SilentPopen(['git', 'submodule', 'foreach', '--recursive'] + cmd, cwd=self.local)
572 if p.returncode != 0:
573 raise VCSException("Git submodule reset failed", p.output)
574 p = SilentPopen(['git', 'submodule', 'sync'], cwd=self.local)
575 if p.returncode != 0:
576 raise VCSException("Git submodule sync failed", p.output)
577 p = FDroidPopen(['git', 'submodule', 'update', '--init', '--force', '--recursive'], cwd=self.local)
578 if p.returncode != 0:
579 raise VCSException("Git submodule update failed", p.output)
583 p = SilentPopen(['git', 'tag'], cwd=self.local)
584 return p.output.splitlines()
586 def latesttags(self, alltags, number):
588 p = SilentPopen(['echo "' + '\n'.join(alltags) + '" | '
589 + 'xargs -I@ git log --format=format:"%at @%n" -1 @ | '
590 + 'sort -n | awk \'{print $2}\''],
591 cwd=self.local, shell=True)
592 return p.output.splitlines()[-number:]
595 class vcs_gitsvn(vcs):
600 # Damn git-svn tries to use a graphical password prompt, so we have to
601 # trick it into taking the password from stdin
603 if self.username is None:
605 return ('echo "%s" | DISPLAY="" ' % self.password, ' --username "%s"' % self.username)
607 # If the local directory exists, but is somehow not a git repository, git
608 # will traverse up the directory tree until it finds one that is (i.e.
609 # fdroidserver) and then we'll proceed to destory it! This is called as
612 p = SilentPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local)
613 result = p.output.rstrip()
614 if not result.endswith(self.local):
615 raise VCSException('Repository mismatch')
617 def gotorevisionx(self, rev):
618 if not os.path.exists(self.local):
620 gitsvn_cmd = '%sgit svn clone%s' % self.userargs()
621 if ';' in self.remote:
622 remote_split = self.remote.split(';')
623 for i in remote_split[1:]:
624 if i.startswith('trunk='):
625 gitsvn_cmd += ' -T %s' % i[6:]
626 elif i.startswith('tags='):
627 gitsvn_cmd += ' -t %s' % i[5:]
628 elif i.startswith('branches='):
629 gitsvn_cmd += ' -b %s' % i[9:]
630 p = SilentPopen([gitsvn_cmd + " %s %s" % (remote_split[0], self.local)], shell=True)
631 if p.returncode != 0:
632 self.clone_failed = True
633 raise VCSException("Git svn clone failed", p.output)
635 p = SilentPopen([gitsvn_cmd + " %s %s" % (self.remote, self.local)], shell=True)
636 if p.returncode != 0:
637 self.clone_failed = True
638 raise VCSException("Git svn clone failed", p.output)
642 # Discard any working tree changes
643 p = SilentPopen(['git', 'reset', '--hard'], cwd=self.local)
644 if p.returncode != 0:
645 raise VCSException("Git reset failed", p.output)
646 # Remove untracked files now, in case they're tracked in the target
647 # revision (it happens!)
648 p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
649 if p.returncode != 0:
650 raise VCSException("Git clean failed", p.output)
651 if not self.refreshed:
652 # Get new commits, branches and tags from repo
653 p = SilentPopen(['%sgit svn fetch %s' % self.userargs()], cwd=self.local, shell=True)
654 if p.returncode != 0:
655 raise VCSException("Git svn fetch failed")
656 p = SilentPopen(['%sgit svn rebase %s' % self.userargs()], cwd=self.local, shell=True)
657 if p.returncode != 0:
658 raise VCSException("Git svn rebase failed", p.output)
659 self.refreshed = True
661 rev = rev or 'master'
663 nospaces_rev = rev.replace(' ', '%20')
664 # Try finding a svn tag
665 for treeish in ['origin/', '']:
666 p = SilentPopen(['git', 'checkout', treeish + 'tags/' + nospaces_rev],
668 if p.returncode == 0:
670 if p.returncode != 0:
671 # No tag found, normal svn rev translation
672 # Translate svn rev into git format
673 rev_split = rev.split('/')
676 for treeish in ['origin/', '']:
677 if len(rev_split) > 1:
678 treeish += rev_split[0]
679 svn_rev = rev_split[1]
682 # if no branch is specified, then assume trunk (i.e. 'master' branch):
686 svn_rev = svn_rev if svn_rev[0] == 'r' else 'r' + svn_rev
688 p = SilentPopen(['git', 'svn', 'find-rev', '--before', svn_rev, treeish],
690 git_rev = p.output.rstrip()
692 if p.returncode == 0 and git_rev:
695 if p.returncode != 0 or not git_rev:
696 # Try a plain git checkout as a last resort
697 p = SilentPopen(['git', 'checkout', rev], cwd=self.local)
698 if p.returncode != 0:
699 raise VCSException("No git treeish found and direct git checkout of '%s' failed" % rev, p.output)
701 # Check out the git rev equivalent to the svn rev
702 p = SilentPopen(['git', 'checkout', git_rev], cwd=self.local)
703 if p.returncode != 0:
704 raise VCSException("Git checkout of '%s' failed" % rev, p.output)
706 # Get rid of any uncontrolled files left behind
707 p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
708 if p.returncode != 0:
709 raise VCSException("Git clean failed", p.output)
713 for treeish in ['origin/', '']:
714 d = os.path.join(self.local, '.git', 'svn', 'refs', 'remotes', treeish, 'tags')
720 p = SilentPopen(['git', 'svn', 'find-rev', 'HEAD'], cwd=self.local)
721 if p.returncode != 0:
723 return p.output.strip()
731 def gotorevisionx(self, rev):
732 if not os.path.exists(self.local):
733 p = SilentPopen(['hg', 'clone', self.remote, self.local])
734 if p.returncode != 0:
735 self.clone_failed = True
736 raise VCSException("Hg clone failed", p.output)
738 p = SilentPopen(['hg status -uS | xargs rm -rf'], cwd=self.local, shell=True)
739 if p.returncode != 0:
740 raise VCSException("Hg clean failed", p.output)
741 if not self.refreshed:
742 p = SilentPopen(['hg', 'pull'], cwd=self.local)
743 if p.returncode != 0:
744 raise VCSException("Hg pull failed", p.output)
745 self.refreshed = True
747 rev = rev or 'default'
750 p = SilentPopen(['hg', 'update', '-C', rev], cwd=self.local)
751 if p.returncode != 0:
752 raise VCSException("Hg checkout of '%s' failed" % rev, p.output)
753 p = SilentPopen(['hg', 'purge', '--all'], cwd=self.local)
754 # Also delete untracked files, we have to enable purge extension for that:
755 if "'purge' is provided by the following extension" in p.output:
756 with open(os.path.join(self.local, '.hg', 'hgrc'), "a") as myfile:
757 myfile.write("\n[extensions]\nhgext.purge=\n")
758 p = SilentPopen(['hg', 'purge', '--all'], cwd=self.local)
759 if p.returncode != 0:
760 raise VCSException("HG purge failed", p.output)
761 elif p.returncode != 0:
762 raise VCSException("HG purge failed", p.output)
765 p = SilentPopen(['hg', 'tags', '-q'], cwd=self.local)
766 return p.output.splitlines()[1:]
774 def gotorevisionx(self, rev):
775 if not os.path.exists(self.local):
776 p = SilentPopen(['bzr', 'branch', self.remote, self.local])
777 if p.returncode != 0:
778 self.clone_failed = True
779 raise VCSException("Bzr branch failed", p.output)
781 p = SilentPopen(['bzr', 'clean-tree', '--force', '--unknown', '--ignored'], cwd=self.local)
782 if p.returncode != 0:
783 raise VCSException("Bzr revert failed", p.output)
784 if not self.refreshed:
785 p = SilentPopen(['bzr', 'pull'], cwd=self.local)
786 if p.returncode != 0:
787 raise VCSException("Bzr update failed", p.output)
788 self.refreshed = True
790 revargs = list(['-r', rev] if rev else [])
791 p = SilentPopen(['bzr', 'revert'] + revargs, cwd=self.local)
792 if p.returncode != 0:
793 raise VCSException("Bzr revert of '%s' failed" % rev, p.output)
796 p = SilentPopen(['bzr', 'tags'], cwd=self.local)
797 return [tag.split(' ')[0].strip() for tag in
798 p.output.splitlines()]
801 def retrieve_string(app_dir, string, xmlfiles=None):
804 os.path.join(app_dir, 'res'),
805 os.path.join(app_dir, 'src', 'main'),
810 for res_dir in res_dirs:
811 for r, d, f in os.walk(res_dir):
812 if os.path.basename(r) == 'values':
813 xmlfiles += [os.path.join(r, x) for x in f if x.endswith('.xml')]
816 if string.startswith('@string/'):
817 string_search = re.compile(r'.*name="' + string[8:] + '".*?>"?([^<]+?)"?<.*').search
818 elif string.startswith('&') and string.endswith(';'):
819 string_search = re.compile(r'.*<!ENTITY.*' + string[1:-1] + '.*?"([^"]+?)".*>').search
821 if string_search is not None:
822 for xmlfile in xmlfiles:
823 for line in file(xmlfile):
824 matches = string_search(line)
826 return retrieve_string(app_dir, matches.group(1), xmlfiles)
829 return string.replace("\\'", "'")
832 # Return list of existing files that will be used to find the highest vercode
833 def manifest_paths(app_dir, flavours):
835 possible_manifests = \
836 [os.path.join(app_dir, 'AndroidManifest.xml'),
837 os.path.join(app_dir, 'src', 'main', 'AndroidManifest.xml'),
838 os.path.join(app_dir, 'src', 'AndroidManifest.xml'),
839 os.path.join(app_dir, 'build.gradle')]
841 for flavour in flavours:
842 possible_manifests.append(
843 os.path.join(app_dir, 'src', flavour, 'AndroidManifest.xml'))
845 return [path for path in possible_manifests if os.path.isfile(path)]
848 # Retrieve the package name. Returns the name, or None if not found.
849 def fetch_real_name(app_dir, flavours):
850 app_search = re.compile(r'.*<application.*').search
851 name_search = re.compile(r'.*android:label="([^"]+)".*').search
853 for f in manifest_paths(app_dir, flavours):
854 if not has_extension(f, 'xml'):
856 logging.debug("fetch_real_name: Checking manifest at " + f)
862 matches = name_search(line)
864 stringname = matches.group(1)
865 logging.debug("fetch_real_name: using string " + stringname)
866 result = retrieve_string(app_dir, stringname)
868 result = result.strip()
873 # Retrieve the version name
874 def version_name(original, app_dir, flavours):
875 for f in manifest_paths(app_dir, flavours):
876 if not has_extension(f, 'xml'):
878 string = retrieve_string(app_dir, original)
884 def get_library_references(root_dir):
886 proppath = os.path.join(root_dir, 'project.properties')
887 if not os.path.isfile(proppath):
889 with open(proppath) as f:
890 for line in f.readlines():
891 if not line.startswith('android.library.reference.'):
893 path = line.split('=')[1].strip()
894 relpath = os.path.join(root_dir, path)
895 if not os.path.isdir(relpath):
897 logging.debug("Found subproject at %s" % path)
898 libraries.append(path)
902 def ant_subprojects(root_dir):
903 subprojects = get_library_references(root_dir)
904 for subpath in subprojects:
905 subrelpath = os.path.join(root_dir, subpath)
906 for p in get_library_references(subrelpath):
907 relp = os.path.normpath(os.path.join(subpath, p))
908 if relp not in subprojects:
909 subprojects.insert(0, relp)
913 def remove_debuggable_flags(root_dir):
914 # Remove forced debuggable flags
915 logging.debug("Removing debuggable flags from %s" % root_dir)
916 for root, dirs, files in os.walk(root_dir):
917 if 'AndroidManifest.xml' in files:
918 path = os.path.join(root, 'AndroidManifest.xml')
919 p = SilentPopen(['sed', '-i', 's/android:debuggable="[^"]*"//g', path])
920 if p.returncode != 0:
921 raise BuildException("Failed to remove debuggable flags of %s" % path)
924 # Extract some information from the AndroidManifest.xml at the given path.
925 # Returns (version, vercode, package), any or all of which might be None.
926 # All values returned are strings.
927 def parse_androidmanifests(paths, ignoreversions=None):
930 return (None, None, None)
932 vcsearch = re.compile(r'.*:versionCode="([0-9]+?)".*').search
933 vnsearch = re.compile(r'.*:versionName="([^"]+?)".*').search
934 psearch = re.compile(r'.*package="([^"]+)".*').search
936 vcsearch_g = re.compile(r'.*versionCode *=* *["\']*([0-9]+)["\']*').search
937 vnsearch_g = re.compile(r'.*versionName *=* *(["\'])((?:(?=(\\?))\3.)*?)\1.*').search
938 psearch_g = re.compile(r'.*packageName *=* *["\']([^"]+)["\'].*').search
940 ignoresearch = re.compile(ignoreversions).search if ignoreversions else None
948 gradle = has_extension(path, 'gradle')
951 # Remember package name, may be defined separately from version+vercode
952 package = max_package
954 for line in file(path):
957 matches = psearch_g(line)
959 matches = psearch(line)
961 package = matches.group(1)
964 matches = vnsearch_g(line)
966 matches = vnsearch(line)
968 version = matches.group(2 if gradle else 1)
971 matches = vcsearch_g(line)
973 matches = vcsearch(line)
975 vercode = matches.group(1)
977 # Always grab the package name and version name in case they are not
978 # together with the highest version code
979 if max_package is None and package is not None:
980 max_package = package
981 if max_version is None and version is not None:
982 max_version = version
984 if max_vercode is None or (vercode is not None and vercode > max_vercode):
985 if not ignoresearch or not ignoresearch(version):
986 if version is not None:
987 max_version = version
988 if vercode is not None:
989 max_vercode = vercode
990 if package is not None:
991 max_package = package
993 max_version = "Ignore"
995 if max_version is None:
996 max_version = "Unknown"
998 return (max_version, max_vercode, max_package)
1001 class FDroidException(Exception):
1002 def __init__(self, value, detail=None):
1004 self.detail = detail
1006 def get_wikitext(self):
1007 ret = repr(self.value) + "\n"
1011 txt = self.detail[-8192:] if len(self.detail) > 8192 else self.detail
1019 ret += "\n==== detail begin ====\n%s\n==== detail end ====" % self.detail.strip()
1023 class VCSException(FDroidException):
1027 class BuildException(FDroidException):
1031 # Get the specified source library.
1032 # Returns the path to it. Normally this is the path to be used when referencing
1033 # it, which may be a subdirectory of the actual project. If you want the base
1034 # directory of the project, pass 'basepath=True'.
1035 def getsrclib(spec, srclib_dir, srclibpaths=[], subdir=None,
1036 basepath=False, raw=False, prepare=True, preponly=False):
1044 name, ref = spec.split('@')
1046 number, name = name.split(':', 1)
1048 name, subdir = name.split('/', 1)
1050 if name not in metadata.srclibs:
1051 raise VCSException('srclib ' + name + ' not found.')
1053 srclib = metadata.srclibs[name]
1055 sdir = os.path.join(srclib_dir, name)
1058 vcs = getvcs(srclib["Repo Type"], srclib["Repo"], sdir)
1059 vcs.srclib = (name, number, sdir)
1061 vcs.gotorevision(ref)
1068 libdir = os.path.join(sdir, subdir)
1069 elif srclib["Subdir"]:
1070 for subdir in srclib["Subdir"]:
1071 libdir_candidate = os.path.join(sdir, subdir)
1072 if os.path.exists(libdir_candidate):
1073 libdir = libdir_candidate
1079 if srclib["Srclibs"]:
1081 for lib in srclib["Srclibs"].replace(';', ',').split(','):
1083 for t in srclibpaths:
1088 raise VCSException('Missing recursive srclib %s for %s' % (
1090 place_srclib(libdir, n, s_tuple[2])
1093 remove_signing_keys(sdir)
1094 remove_debuggable_flags(sdir)
1098 if srclib["Prepare"]:
1099 cmd = replace_config_vars(srclib["Prepare"])
1101 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=libdir)
1102 if p.returncode != 0:
1103 raise BuildException("Error running prepare command for srclib %s"
1109 return (name, number, libdir)
1112 # Prepare the source code for a particular build
1113 # 'vcs' - the appropriate vcs object for the application
1114 # 'app' - the application details from the metadata
1115 # 'build' - the build details from the metadata
1116 # 'build_dir' - the path to the build directory, usually
1118 # 'srclib_dir' - the path to the source libraries directory, usually
1120 # 'extlib_dir' - the path to the external libraries directory, usually
1122 # Returns the (root, srclibpaths) where:
1123 # 'root' is the root directory, which may be the same as 'build_dir' or may
1124 # be a subdirectory of it.
1125 # 'srclibpaths' is information on the srclibs being used
1126 def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=False):
1128 # Optionally, the actual app source can be in a subdirectory
1130 root_dir = os.path.join(build_dir, build['subdir'])
1132 root_dir = build_dir
1134 # Get a working copy of the right revision
1135 logging.info("Getting source for revision " + build['commit'])
1136 vcs.gotorevision(build['commit'])
1138 # Initialise submodules if requred
1139 if build['submodules']:
1140 logging.info("Initialising submodules")
1141 vcs.initsubmodules()
1143 # Check that a subdir (if we're using one) exists. This has to happen
1144 # after the checkout, since it might not exist elsewhere
1145 if not os.path.exists(root_dir):
1146 raise BuildException('Missing subdir ' + root_dir)
1148 # Run an init command if one is required
1150 cmd = replace_config_vars(build['init'])
1151 logging.info("Running 'init' commands in %s" % root_dir)
1153 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
1154 if p.returncode != 0:
1155 raise BuildException("Error running init command for %s:%s" %
1156 (app['id'], build['version']), p.output)
1158 # Apply patches if any
1160 logging.info("Applying patches")
1161 for patch in build['patch']:
1162 patch = patch.strip()
1163 logging.info("Applying " + patch)
1164 patch_path = os.path.join('metadata', app['id'], patch)
1165 p = FDroidPopen(['patch', '-p1', '-i', os.path.abspath(patch_path)], cwd=build_dir)
1166 if p.returncode != 0:
1167 raise BuildException("Failed to apply patch %s" % patch_path)
1169 # Get required source libraries
1171 if build['srclibs']:
1172 logging.info("Collecting source libraries")
1173 for lib in build['srclibs']:
1174 srclibpaths.append(getsrclib(lib, srclib_dir, srclibpaths,
1177 for name, number, libpath in srclibpaths:
1178 place_srclib(root_dir, int(number) if number else None, libpath)
1180 basesrclib = vcs.getsrclib()
1181 # If one was used for the main source, add that too.
1183 srclibpaths.append(basesrclib)
1185 # Update the local.properties file
1186 localprops = [os.path.join(build_dir, 'local.properties')]
1188 localprops += [os.path.join(root_dir, 'local.properties')]
1189 for path in localprops:
1190 if not os.path.isfile(path):
1192 logging.info("Updating properties file at %s" % path)
1197 # Fix old-fashioned 'sdk-location' by copying
1198 # from sdk.dir, if necessary
1199 if build['oldsdkloc']:
1200 sdkloc = re.match(r".*^sdk.dir=(\S+)$.*", props,
1201 re.S | re.M).group(1)
1202 props += "sdk-location=%s\n" % sdkloc
1204 props += "sdk.dir=%s\n" % config['sdk_path']
1205 props += "sdk-location=%s\n" % config['sdk_path']
1206 if 'ndk_path' in config:
1208 props += "ndk.dir=%s\n" % config['ndk_path']
1209 props += "ndk-location=%s\n" % config['ndk_path']
1210 # Add java.encoding if necessary
1211 if build['encoding']:
1212 props += "java.encoding=%s\n" % build['encoding']
1218 if build['type'] == 'gradle':
1219 flavours = build['gradle']
1221 version_regex = re.compile(r".*'com\.android\.tools\.build:gradle:([^\.]+\.[^\.]+).*'.*")
1222 gradlepluginver = None
1224 gradle_files = [os.path.join(root_dir, 'build.gradle')]
1226 # Parent dir build.gradle
1227 parent_dir = os.path.normpath(os.path.join(root_dir, '..'))
1228 if parent_dir.startswith(build_dir):
1229 gradle_files.append(os.path.join(parent_dir, 'build.gradle'))
1231 for path in gradle_files:
1234 if not os.path.isfile(path):
1236 with open(path) as f:
1238 match = version_regex.match(line)
1240 gradlepluginver = match.group(1)
1244 build['gradlepluginver'] = LooseVersion(gradlepluginver)
1246 logging.warn("Could not fetch the gradle plugin version, defaulting to 0.11")
1247 build['gradlepluginver'] = LooseVersion('0.11')
1250 n = build["target"].split('-')[1]
1251 SilentPopen(['sed', '-i',
1252 's@compileSdkVersion *[0-9]*@compileSdkVersion ' + n + '@g',
1256 # Remove forced debuggable flags
1257 remove_debuggable_flags(root_dir)
1259 # Insert version code and number into the manifest if necessary
1260 if build['forceversion']:
1261 logging.info("Changing the version name")
1262 for path in manifest_paths(root_dir, flavours):
1263 if not os.path.isfile(path):
1265 if has_extension(path, 'xml'):
1266 p = SilentPopen(['sed', '-i',
1267 's/android:versionName="[^"]*"/android:versionName="'
1268 + build['version'] + '"/g',
1270 if p.returncode != 0:
1271 raise BuildException("Failed to amend manifest")
1272 elif has_extension(path, 'gradle'):
1273 p = SilentPopen(['sed', '-i',
1274 's/versionName *=* *"[^"]*"/versionName = "'
1275 + build['version'] + '"/g',
1277 if p.returncode != 0:
1278 raise BuildException("Failed to amend build.gradle")
1279 if build['forcevercode']:
1280 logging.info("Changing the version code")
1281 for path in manifest_paths(root_dir, flavours):
1282 if not os.path.isfile(path):
1284 if has_extension(path, 'xml'):
1285 p = SilentPopen(['sed', '-i',
1286 's/android:versionCode="[^"]*"/android:versionCode="'
1287 + build['vercode'] + '"/g',
1289 if p.returncode != 0:
1290 raise BuildException("Failed to amend manifest")
1291 elif has_extension(path, 'gradle'):
1292 p = SilentPopen(['sed', '-i',
1293 's/versionCode *=* *[0-9]*/versionCode = '
1294 + build['vercode'] + '/g',
1296 if p.returncode != 0:
1297 raise BuildException("Failed to amend build.gradle")
1299 # Delete unwanted files
1301 logging.info("Removing specified files")
1302 for part in getpaths(build_dir, build, 'rm'):
1303 dest = os.path.join(build_dir, part)
1304 logging.info("Removing {0}".format(part))
1305 if os.path.lexists(dest):
1306 if os.path.islink(dest):
1307 SilentPopen(['unlink ' + dest], shell=True)
1309 SilentPopen(['rm -rf ' + dest], shell=True)
1311 logging.info("...but it didn't exist")
1313 remove_signing_keys(build_dir)
1315 # Add required external libraries
1316 if build['extlibs']:
1317 logging.info("Collecting prebuilt libraries")
1318 libsdir = os.path.join(root_dir, 'libs')
1319 if not os.path.exists(libsdir):
1321 for lib in build['extlibs']:
1323 logging.info("...installing extlib {0}".format(lib))
1324 libf = os.path.basename(lib)
1325 libsrc = os.path.join(extlib_dir, lib)
1326 if not os.path.exists(libsrc):
1327 raise BuildException("Missing extlib file {0}".format(libsrc))
1328 shutil.copyfile(libsrc, os.path.join(libsdir, libf))
1330 # Run a pre-build command if one is required
1331 if build['prebuild']:
1332 logging.info("Running 'prebuild' commands in %s" % root_dir)
1334 cmd = replace_config_vars(build['prebuild'])
1336 # Substitute source library paths into prebuild commands
1337 for name, number, libpath in srclibpaths:
1338 libpath = os.path.relpath(libpath, root_dir)
1339 cmd = cmd.replace('$$' + name + '$$', libpath)
1341 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
1342 if p.returncode != 0:
1343 raise BuildException("Error running prebuild command for %s:%s" %
1344 (app['id'], build['version']), p.output)
1346 # Generate (or update) the ant build file, build.xml...
1347 if build['update'] and build['update'] != ['no'] and build['type'] == 'ant':
1348 parms = [config['android'], 'update', 'lib-project']
1349 lparms = [config['android'], 'update', 'project']
1352 parms += ['-t', build['target']]
1353 lparms += ['-t', build['target']]
1354 if build['update'] == ['auto']:
1355 update_dirs = ant_subprojects(root_dir) + ['.']
1357 update_dirs = build['update']
1359 for d in update_dirs:
1360 subdir = os.path.join(root_dir, d)
1362 logging.debug("Updating main project")
1363 cmd = parms + ['-p', d]
1365 logging.debug("Updating subproject %s" % d)
1366 cmd = lparms + ['-p', d]
1367 p = FDroidPopen(cmd, cwd=root_dir)
1368 # Check to see whether an error was returned without a proper exit
1369 # code (this is the case for the 'no target set or target invalid'
1371 if p.returncode != 0 or p.output.startswith("Error: "):
1372 raise BuildException("Failed to update project at %s" % d, p.output)
1373 # Clean update dirs via ant
1375 logging.info("Cleaning subproject %s" % d)
1376 p = FDroidPopen(['ant', 'clean'], cwd=subdir)
1378 return (root_dir, srclibpaths)
1381 # Split and extend via globbing the paths from a field
1382 def getpaths(build_dir, build, field):
1384 for p in build[field]:
1386 full_path = os.path.join(build_dir, p)
1387 full_path = os.path.normpath(full_path)
1388 paths += [r[len(build_dir) + 1:] for r in glob.glob(full_path)]
1392 # Scan the source code in the given directory (and all subdirectories)
1393 # and return the number of fatal problems encountered
1394 def scan_source(build_dir, root_dir, thisbuild):
1398 # Common known non-free blobs (always lower case):
1400 re.compile(r'flurryagent', re.IGNORECASE),
1401 re.compile(r'paypal.*mpl', re.IGNORECASE),
1402 re.compile(r'google.*analytics', re.IGNORECASE),
1403 re.compile(r'admob.*sdk.*android', re.IGNORECASE),
1404 re.compile(r'google.*ad.*view', re.IGNORECASE),
1405 re.compile(r'google.*admob', re.IGNORECASE),
1406 re.compile(r'google.*play.*services', re.IGNORECASE),
1407 re.compile(r'crittercism', re.IGNORECASE),
1408 re.compile(r'heyzap', re.IGNORECASE),
1409 re.compile(r'jpct.*ae', re.IGNORECASE),
1410 re.compile(r'youtube.*android.*player.*api', re.IGNORECASE),
1411 re.compile(r'bugsense', re.IGNORECASE),
1412 re.compile(r'crashlytics', re.IGNORECASE),
1413 re.compile(r'ouya.*sdk', re.IGNORECASE),
1414 re.compile(r'libspen23', re.IGNORECASE),
1417 scanignore = getpaths(build_dir, thisbuild, 'scanignore')
1418 scandelete = getpaths(build_dir, thisbuild, 'scandelete')
1421 ms = magic.open(magic.MIME_TYPE)
1423 except AttributeError:
1427 for i in scanignore:
1428 if fd.startswith(i):
1433 for i in scandelete:
1434 if fd.startswith(i):
1438 def removeproblem(what, fd, fp):
1439 logging.info('Removing %s at %s' % (what, fd))
1442 def warnproblem(what, fd):
1443 logging.warn('Found %s at %s' % (what, fd))
1445 def handleproblem(what, fd, fp):
1447 logging.info('Ignoring %s at %s' % (what, fd))
1449 removeproblem(what, fd, fp)
1451 logging.error('Found %s at %s' % (what, fd))
1455 # Iterate through all files in the source code
1456 for r, d, f in os.walk(build_dir, topdown=True):
1458 # It's topdown, so checking the basename is enough
1459 for ignoredir in ('.hg', '.git', '.svn', '.bzr'):
1465 # Path (relative) to the file
1466 fp = os.path.join(r, curfile)
1467 fd = fp[len(build_dir) + 1:]
1470 mime = magic.from_file(fp, mime=True) if ms is None else ms.file(fp)
1471 except UnicodeError:
1472 warnproblem('malformed magic number', fd)
1474 if mime == 'application/x-sharedlib':
1475 count += handleproblem('shared library', fd, fp)
1477 elif mime == 'application/x-archive':
1478 count += handleproblem('static library', fd, fp)
1480 elif mime == 'application/x-executable':
1481 count += handleproblem('binary executable', fd, fp)
1483 elif mime == 'application/x-java-applet':
1484 count += handleproblem('Java compiled class', fd, fp)
1489 'application/java-archive',
1490 'application/octet-stream',
1494 if has_extension(fp, 'apk'):
1495 removeproblem('APK file', fd, fp)
1497 elif has_extension(fp, 'jar'):
1499 if any(suspect.match(curfile) for suspect in usual_suspects):
1500 count += handleproblem('usual supect', fd, fp)
1502 warnproblem('JAR file', fd)
1504 elif has_extension(fp, 'zip'):
1505 warnproblem('ZIP file', fd)
1508 warnproblem('unknown compressed or binary file', fd)
1510 elif has_extension(fp, 'java'):
1511 for line in file(fp):
1512 if 'DexClassLoader' in line:
1513 count += handleproblem('DexClassLoader', fd, fp)
1518 # Presence of a jni directory without buildjni=yes might
1519 # indicate a problem (if it's not a problem, explicitly use
1520 # buildjni=no to bypass this check)
1521 if (os.path.exists(os.path.join(root_dir, 'jni')) and
1522 not thisbuild['buildjni']):
1523 logging.error('Found jni directory, but buildjni is not enabled. Set it to \'no\' to ignore.')
1532 self.path = os.path.join('stats', 'known_apks.txt')
1534 if os.path.exists(self.path):
1535 for line in file(self.path):
1536 t = line.rstrip().split(' ')
1538 self.apks[t[0]] = (t[1], None)
1540 self.apks[t[0]] = (t[1], time.strptime(t[2], '%Y-%m-%d'))
1541 self.changed = False
1543 def writeifchanged(self):
1545 if not os.path.exists('stats'):
1547 f = open(self.path, 'w')
1549 for apk, app in self.apks.iteritems():
1551 line = apk + ' ' + appid
1553 line += ' ' + time.strftime('%Y-%m-%d', added)
1555 for line in sorted(lst):
1556 f.write(line + '\n')
1559 # Record an apk (if it's new, otherwise does nothing)
1560 # Returns the date it was added.
1561 def recordapk(self, apk, app):
1562 if apk not in self.apks:
1563 self.apks[apk] = (app, time.gmtime(time.time()))
1565 _, added = self.apks[apk]
1568 # Look up information - given the 'apkname', returns (app id, date added/None).
1569 # Or returns None for an unknown apk.
1570 def getapp(self, apkname):
1571 if apkname in self.apks:
1572 return self.apks[apkname]
1575 # Get the most recent 'num' apps added to the repo, as a list of package ids
1576 # with the most recent first.
1577 def getlatest(self, num):
1579 for apk, app in self.apks.iteritems():
1583 if apps[appid] > added:
1587 sortedapps = sorted(apps.iteritems(), key=operator.itemgetter(1))[-num:]
1588 lst = [app for app, _ in sortedapps]
1593 def isApkDebuggable(apkfile, config):
1594 """Returns True if the given apk file is debuggable
1596 :param apkfile: full path to the apk to check"""
1598 p = SilentPopen([os.path.join(config['sdk_path'], 'build-tools',
1599 config['build_tools'], 'aapt'),
1600 'dump', 'xmltree', apkfile, 'AndroidManifest.xml'])
1601 if p.returncode != 0:
1602 logging.critical("Failed to get apk manifest information")
1604 for line in p.output.splitlines():
1605 if 'android:debuggable' in line and not line.endswith('0x0'):
1610 class AsynchronousFileReader(threading.Thread):
1612 Helper class to implement asynchronous reading of a file
1613 in a separate thread. Pushes read lines on a queue to
1614 be consumed in another thread.
1617 def __init__(self, fd, queue):
1618 assert isinstance(queue, Queue.Queue)
1619 assert callable(fd.readline)
1620 threading.Thread.__init__(self)
1625 '''The body of the tread: read lines and put them on the queue.'''
1626 for line in iter(self._fd.readline, ''):
1627 self._queue.put(line)
1630 '''Check whether there is no more content to expect.'''
1631 return not self.is_alive() and self._queue.empty()
1639 def SilentPopen(commands, cwd=None, shell=False):
1640 return FDroidPopen(commands, cwd=cwd, shell=shell, output=False)
1643 def FDroidPopen(commands, cwd=None, shell=False, output=True):
1645 Run a command and capture the possibly huge output.
1647 :param commands: command and argument list like in subprocess.Popen
1648 :param cwd: optionally specifies a working directory
1649 :returns: A PopenResult.
1655 cwd = os.path.normpath(cwd)
1656 logging.debug("Directory: %s" % cwd)
1657 logging.debug("> %s" % ' '.join(commands))
1659 result = PopenResult()
1662 p = subprocess.Popen(commands, cwd=cwd, shell=shell, env=env,
1663 stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
1665 raise BuildException("OSError while trying to execute " + ' '.join(commands) + ': ' + str(e))
1667 stdout_queue = Queue.Queue()
1668 stdout_reader = AsynchronousFileReader(p.stdout, stdout_queue)
1669 stdout_reader.start()
1671 # Check the queue for output (until there is no more to get)
1672 while not stdout_reader.eof():
1673 while not stdout_queue.empty():
1674 line = stdout_queue.get()
1675 if output and options.verbose:
1676 # Output directly to console
1677 sys.stderr.write(line)
1679 result.output += line
1683 result.returncode = p.wait()
1687 def remove_signing_keys(build_dir):
1688 comment = re.compile(r'[ ]*//')
1689 signing_configs = re.compile(r'^[\t ]*signingConfigs[ \t]*{[ \t]*$')
1691 re.compile(r'^[\t ]*signingConfig [^ ]*$'),
1692 re.compile(r'.*android\.signingConfigs\.[^{]*$'),
1693 re.compile(r'.*variant\.outputFile = .*'),
1694 re.compile(r'.*\.readLine\(.*'),
1696 for root, dirs, files in os.walk(build_dir):
1697 if 'build.gradle' in files:
1698 path = os.path.join(root, 'build.gradle')
1700 with open(path, "r") as o:
1701 lines = o.readlines()
1706 with open(path, "w") as o:
1708 if comment.match(line):
1712 opened += line.count('{')
1713 opened -= line.count('}')
1716 if signing_configs.match(line):
1721 if any(s.match(line) for s in line_matches):
1729 logging.info("Cleaned build.gradle of keysigning configs at %s" % path)
1732 'project.properties',
1734 'default.properties',
1737 if propfile in files:
1738 path = os.path.join(root, propfile)
1740 with open(path, "r") as o:
1741 lines = o.readlines()
1745 with open(path, "w") as o:
1747 if any(line.startswith(s) for s in ('key.store', 'key.alias')):
1754 logging.info("Cleaned %s of keysigning configs at %s" % (propfile, path))
1757 def replace_config_vars(cmd):
1758 cmd = cmd.replace('$$SDK$$', config['sdk_path'])
1759 cmd = cmd.replace('$$NDK$$', config['ndk_path'])
1760 cmd = cmd.replace('$$MVN3$$', config['mvn3'])
1764 def place_srclib(root_dir, number, libpath):
1767 relpath = os.path.relpath(libpath, root_dir)
1768 proppath = os.path.join(root_dir, 'project.properties')
1771 if os.path.isfile(proppath):
1772 with open(proppath, "r") as o:
1773 lines = o.readlines()
1775 with open(proppath, "w") as o:
1778 if line.startswith('android.library.reference.%d=' % number):
1779 o.write('android.library.reference.%d=%s\n' % (number, relpath))
1784 o.write('android.library.reference.%d=%s\n' % (number, relpath))