chiark / gitweb /
Don't look for strings in unrelated projects
[fdroidserver.git] / fdroidserver / common.py
1 # -*- coding: utf-8 -*-
2 #
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>
6 #
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.
11 #
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.
16 #
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/>.
19
20 import os, sys, re
21 import shutil
22 import stat
23 import subprocess
24 import time
25 import operator
26 import Queue
27 import threading
28 import magic
29 import logging
30
31 import metadata
32
33 config = None
34 options = None
35
36 def read_config(opts, config_file='config.py'):
37     """Read the repository config
38
39     The config is read from config_file, which is in the current directory when
40     any of the repo management commands are used.
41     """
42     global config, options
43
44     if config is not None:
45         return config
46     if not os.path.isfile(config_file):
47         logging.critical("Missing config file - is this a repo directory?")
48         sys.exit(2)
49
50     options = opts
51
52     config = {}
53
54     logging.info("Reading %s" % config_file)
55     execfile(config_file, config)
56
57     defconfig = {
58         'sdk_path': "$ANDROID_HOME",
59         'ndk_path': "$ANDROID_NDK",
60         'build_tools': "19.0.3",
61         'ant': "ant",
62         'mvn3': "mvn",
63         'gradle': 'gradle',
64         'archive_older': 0,
65         'update_stats': False,
66         'stats_to_carbon': False,
67         'repo_maxage': 0,
68         'build_server_always': False,
69         'char_limits': {
70             'Summary' : 50,
71             'Description' : 1500
72         }
73     }
74     for k, v in defconfig.items():
75         if k not in config:
76             config[k] = v
77
78     # Expand environment variables
79     for k, v in config.items():
80         if type(v) != str:
81             continue
82         v = os.path.expanduser(v)
83         config[k] = os.path.expandvars(v)
84
85     if not config['sdk_path']:
86         logging.critical("$ANDROID_HOME is not set!")
87         sys.exit(3)
88     if not os.path.isdir(config['sdk_path']):
89         logging.critical("$ANDROID_HOME points to a non-existing directory!")
90         sys.exit(3)
91
92     if any(k in config for k in ["keystore", "keystorepass", "keypass"]):
93         st = os.stat(config_file)
94         if st.st_mode & stat.S_IRWXG or st.st_mode & stat.S_IRWXO:
95             logging.warn("unsafe permissions on {0} (should be 0600)!".format(config_file))
96
97     return config
98
99 # Given the arguments in the form of multiple appid:[vc] strings, this returns
100 # a dictionary with the set of vercodes specified for each package.
101 def read_pkg_args(args, allow_vercodes=False):
102
103     vercodes = {}
104     if not args:
105         return vercodes
106
107     for p in args:
108         if allow_vercodes and ':' in p:
109             package, vercode = p.split(':')
110         else:
111             package, vercode = p, None
112         if package not in vercodes:
113             vercodes[package] = [vercode] if vercode else []
114             continue
115         elif vercode and vercode not in vercodes[package]:
116             vercodes[package] += [vercode] if vercode else []
117
118     return vercodes
119
120 # On top of what read_pkg_args does, this returns the whole app metadata, but
121 # limiting the builds list to the builds matching the vercodes specified.
122 def read_app_args(args, allapps, allow_vercodes=False):
123
124     vercodes = read_pkg_args(args, allow_vercodes)
125
126     if not vercodes:
127         return allapps
128
129     apps = [app for app in allapps if app['id'] in vercodes]
130
131     if len(apps) != len(vercodes):
132         allids = [app["id"] for app in allapps]
133         for p in vercodes:
134             if p not in allids:
135                 logging.critical("No such package: %s" % p)
136         raise Exception("Found invalid app ids in arguments")
137     if not apps:
138         raise Exception("No packages specified")
139
140     error = False
141     for app in apps:
142         vc = vercodes[app['id']]
143         if not vc:
144             continue
145         app['builds'] = [b for b in app['builds'] if b['vercode'] in vc]
146         if len(app['builds']) != len(vercodes[app['id']]):
147             error = True
148             allvcs = [b['vercode'] for b in app['builds']]
149             for v in vercodes[app['id']]:
150                 if v not in allvcs:
151                     logging.critical("No such vercode %s for app %s" % (v, app['id']))
152
153     if error:
154         raise Exception("Found invalid vercodes for some apps")
155
156     return apps
157
158 def has_extension(filename, extension):
159     name, ext = os.path.splitext(filename)
160     ext = ext.lower()[1:]
161     return ext == extension
162
163 apk_regex = None
164
165 def apknameinfo(filename):
166     global apk_regex
167     filename = os.path.basename(filename)
168     if apk_regex is None:
169         apk_regex = re.compile(r"^(.+)_([0-9]+)\.apk$")
170     m = apk_regex.match(filename)
171     try:
172         result = (m.group(1), m.group(2))
173     except AttributeError:
174         raise Exception("Invalid apk name: %s" % filename)
175     return result
176
177 def getapkname(app, build):
178     return "%s_%s.apk" % (app['id'], build['vercode'])
179
180 def getsrcname(app, build):
181     return "%s_%s_src.tar.gz" % (app['id'], build['vercode'])
182
183 def getappname(app):
184     if app['Name']:
185         return app['Name']
186     if app['Auto Name']:
187         return app['Auto Name']
188     return app['id']
189
190 def getcvname(app):
191     return '%s (%s)' % (app['Current Version'], app['Current Version Code'])
192
193 def getvcs(vcstype, remote, local):
194     if vcstype == 'git':
195         return vcs_git(remote, local)
196     if vcstype == 'svn':
197         return vcs_svn(remote, local)
198     if vcstype == 'git-svn':
199         return vcs_gitsvn(remote, local)
200     if vcstype == 'hg':
201         return vcs_hg(remote, local)
202     if vcstype == 'bzr':
203         return vcs_bzr(remote, local)
204     if vcstype == 'srclib':
205         if local != 'build/srclib/' + remote:
206             raise VCSException("Error: srclib paths are hard-coded!")
207         return getsrclib(remote, 'build/srclib', raw=True)
208     raise VCSException("Invalid vcs type " + vcstype)
209
210 def getsrclibvcs(name):
211     srclib_path = os.path.join('srclibs', name + ".txt")
212     if not os.path.exists(srclib_path):
213         raise VCSException("Missing srclib " + name)
214     return metadata.parse_srclib(srclib_path)['Repo Type']
215
216 class vcs:
217     def __init__(self, remote, local):
218
219         # svn, git-svn and bzr may require auth
220         self.username = None
221         if self.repotype() in ('svn', 'git-svn', 'bzr'):
222             if '@' in remote:
223                 self.username, remote = remote.split('@')
224                 if ':' not in self.username:
225                     raise VCSException("Password required with username")
226                 self.username, self.password = self.username.split(':')
227
228         self.remote = remote
229         self.local = local
230         self.refreshed = False
231         self.srclib = None
232
233     # Take the local repository to a clean version of the given revision, which
234     # is specificed in the VCS's native format. Beforehand, the repository can
235     # be dirty, or even non-existent. If the repository does already exist
236     # locally, it will be updated from the origin, but only once in the
237     # lifetime of the vcs object.
238     # None is acceptable for 'rev' if you know you are cloning a clean copy of
239     # the repo - otherwise it must specify a valid revision.
240     def gotorevision(self, rev):
241
242         # The .fdroidvcs-id file for a repo tells us what VCS type
243         # and remote that directory was created from, allowing us to drop it
244         # automatically if either of those things changes.
245         fdpath = os.path.join(self.local, '..',
246                 '.fdroidvcs-' + os.path.basename(self.local))
247         cdata = self.repotype() + ' ' + self.remote
248         writeback = True
249         deleterepo = False
250         if os.path.exists(self.local):
251             if os.path.exists(fdpath):
252                 with open(fdpath, 'r') as f:
253                     fsdata = f.read().strip()
254                 if fsdata == cdata:
255                     writeback = False
256                 else:
257                     deleterepo = True
258                     logging.info("Repository details changed - deleting")
259             else:
260                 deleterepo = True
261                 logging.info("Repository details missing - deleting")
262         if deleterepo:
263             shutil.rmtree(self.local)
264
265         self.gotorevisionx(rev)
266
267         # If necessary, write the .fdroidvcs file.
268         if writeback:
269             with open(fdpath, 'w') as f:
270                 f.write(cdata)
271
272     # Derived classes need to implement this. It's called once basic checking
273     # has been performend.
274     def gotorevisionx(self, rev):
275         raise VCSException("This VCS type doesn't define gotorevisionx")
276
277     # Initialise and update submodules
278     def initsubmodules(self):
279         raise VCSException('Submodules not supported for this vcs type')
280
281     # Get a list of all known tags
282     def gettags(self):
283         raise VCSException('gettags not supported for this vcs type')
284
285     # Get current commit reference (hash, revision, etc)
286     def getref(self):
287         raise VCSException('getref not supported for this vcs type')
288
289     # Returns the srclib (name, path) used in setting up the current
290     # revision, or None.
291     def getsrclib(self):
292         return self.srclib
293
294 class vcs_git(vcs):
295
296     def repotype(self):
297         return 'git'
298
299     # If the local directory exists, but is somehow not a git repository, git
300     # will traverse up the directory tree until it finds one that is (i.e.
301     # fdroidserver) and then we'll proceed to destroy it! This is called as
302     # a safety check.
303     def checkrepo(self):
304         p = SilentPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local)
305         result = p.stdout.rstrip()
306         if not result.endswith(self.local):
307             raise VCSException('Repository mismatch')
308
309     def gotorevisionx(self, rev):
310         if not os.path.exists(self.local):
311             # Brand new checkout
312             p = SilentPopen(['git', 'clone', self.remote, self.local])
313             if p.returncode != 0:
314                 raise VCSException("Git clone failed")
315             self.checkrepo()
316         else:
317             self.checkrepo()
318             # Discard any working tree changes
319             p = SilentPopen(['git', 'reset', '--hard'], cwd=self.local)
320             if p.returncode != 0:
321                 raise VCSException("Git reset failed")
322             # Remove untracked files now, in case they're tracked in the target
323             # revision (it happens!)
324             p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
325             if p.returncode != 0:
326                 raise VCSException("Git clean failed")
327             if not self.refreshed:
328                 # Get latest commits and tags from remote
329                 p = SilentPopen(['git', 'fetch', 'origin'], cwd=self.local)
330                 if p.returncode != 0:
331                     raise VCSException("Git fetch failed")
332                 p = SilentPopen(['git', 'fetch', '--prune', '--tags', 'origin'], cwd=self.local)
333                 if p.returncode != 0:
334                     raise VCSException("Git fetch failed")
335                 self.refreshed = True
336         # Check out the appropriate revision
337         rev = str(rev if rev else 'origin/master')
338         p = SilentPopen(['git', 'checkout', '-f', rev], cwd=self.local)
339         if p.returncode != 0:
340             raise VCSException("Git checkout failed")
341         # Get rid of any uncontrolled files left behind
342         p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
343         if p.returncode != 0:
344             raise VCSException("Git clean failed")
345
346     def initsubmodules(self):
347         self.checkrepo()
348         for cmd in [
349                 ['git', 'reset', '--hard'],
350                 ['git', 'clean', '-dffx'],
351                 ]:
352             p = SilentPopen(['git', 'submodule', 'foreach', '--recursive'] + cmd, cwd=self.local)
353             if p.returncode != 0:
354                 raise VCSException("Git submodule reset failed")
355         p = SilentPopen(['git', 'submodule', 'update', '--init', '--force', '--recursive'], cwd=self.local)
356         if p.returncode != 0:
357             raise VCSException("Git submodule update failed")
358
359     def gettags(self):
360         self.checkrepo()
361         p = SilentPopen(['git', 'tag'], cwd=self.local)
362         return p.stdout.splitlines()
363
364
365 class vcs_gitsvn(vcs):
366
367     def repotype(self):
368         return 'git-svn'
369
370     # Damn git-svn tries to use a graphical password prompt, so we have to
371     # trick it into taking the password from stdin
372     def userargs(self):
373         if self.username is None:
374             return ('', '')
375         return ('echo "%s" | DISPLAY="" ' % self.password, ' --username "%s"' % self.username)
376
377     # If the local directory exists, but is somehow not a git repository, git
378     # will traverse up the directory tree until it finds one that is (i.e.
379     # fdroidserver) and then we'll proceed to destory it! This is called as
380     # a safety check.
381     def checkrepo(self):
382         p = SilentPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local)
383         result = p.stdout.rstrip()
384         if not result.endswith(self.local):
385             raise VCSException('Repository mismatch')
386
387     def gotorevisionx(self, rev):
388         if not os.path.exists(self.local):
389             # Brand new checkout
390             gitsvn_cmd = '%sgit svn clone%s' % self.userargs()
391             if ';' in self.remote:
392                 remote_split = self.remote.split(';')
393                 for i in remote_split[1:]:
394                     if i.startswith('trunk='):
395                         gitsvn_cmd += ' -T %s' % i[6:]
396                     elif i.startswith('tags='):
397                         gitsvn_cmd += ' -t %s' % i[5:]
398                     elif i.startswith('branches='):
399                         gitsvn_cmd += ' -b %s' % i[9:]
400                 p = SilentPopen([gitsvn_cmd + " %s %s" % (remote_split[0], self.local)], shell=True)
401                 if p.returncode != 0:
402                     raise VCSException("Git clone failed")
403             else:
404                 p = SilentPopen([gitsvn_cmd + " %s %s" % (self.remote, self.local)], shell=True)
405                 if p.returncode != 0:
406                     raise VCSException("Git clone failed")
407             self.checkrepo()
408         else:
409             self.checkrepo()
410             # Discard any working tree changes
411             p = SilentPopen(['git', 'reset', '--hard'], cwd=self.local)
412             if p.returncode != 0:
413                 raise VCSException("Git reset failed")
414             # Remove untracked files now, in case they're tracked in the target
415             # revision (it happens!)
416             p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
417             if p.returncode != 0:
418                 raise VCSException("Git clean failed")
419             if not self.refreshed:
420                 # Get new commits, branches and tags from repo
421                 p = SilentPopen(['%sgit svn fetch %s' % self.userargs()], cwd=self.local, shell=True)
422                 if p.returncode != 0:
423                     raise VCSException("Git svn fetch failed")
424                 p = SilentPopen(['%sgit svn rebase %s' % self.userargs()], cwd=self.local, shell=True)
425                 if p.returncode != 0:
426                     raise VCSException("Git svn rebase failed")
427                 self.refreshed = True
428
429         rev = str(rev if rev else 'master')
430         if rev:
431             nospaces_rev = rev.replace(' ', '%20')
432             # Try finding a svn tag
433             p = SilentPopen(['git', 'checkout', 'tags/' + nospaces_rev], cwd=self.local)
434             if p.returncode != 0:
435                 # No tag found, normal svn rev translation
436                 # Translate svn rev into git format
437                 rev_split = rev.split('/')
438                 if len(rev_split) > 1:
439                     treeish = rev_split[0]
440                     svn_rev = rev_split[1]
441
442                 else:
443                     # if no branch is specified, then assume trunk (ie. 'master' 
444                     # branch):
445                     treeish = 'master'
446                     svn_rev = rev
447
448                 p = SilentPopen(['git', 'svn', 'find-rev', 'r' + svn_rev, treeish], cwd=self.local)
449                 git_rev = p.stdout.rstrip()
450
451                 if p.returncode != 0 or not git_rev:
452                     # Try a plain git checkout as a last resort
453                     p = SilentPopen(['git', 'checkout', rev], cwd=self.local)
454                     if p.returncode != 0:
455                         raise VCSException("No git treeish found and direct git checkout failed")
456                 else:
457                     # Check out the git rev equivalent to the svn rev
458                     p = SilentPopen(['git', 'checkout', git_rev], cwd=self.local)
459                     if p.returncode != 0:
460                         raise VCSException("Git svn checkout failed")
461
462         # Get rid of any uncontrolled files left behind
463         p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
464         if p.returncode != 0:
465             raise VCSException("Git clean failed")
466
467     def gettags(self):
468         self.checkrepo()
469         return os.listdir(os.path.join(self.local, '.git/svn/refs/remotes/tags'))
470
471     def getref(self):
472         self.checkrepo()
473         p = SilentPopen(['git', 'svn', 'find-rev', 'HEAD'], cwd=self.local)
474         if p.returncode != 0:
475             return None
476         return p.stdout.strip()
477
478 class vcs_svn(vcs):
479
480     def repotype(self):
481         return 'svn'
482
483     def userargs(self):
484         if self.username is None:
485             return ['--non-interactive']
486         return ['--username', self.username,
487                 '--password', self.password,
488                 '--non-interactive']
489
490     def gotorevisionx(self, rev):
491         if not os.path.exists(self.local):
492             p = SilentPopen(['svn', 'checkout', self.remote, self.local] + self.userargs())
493             if p.returncode != 0:
494                 raise VCSException("Svn checkout failed")
495         else:
496             for svncommand in (
497                     'svn revert -R .',
498                     r"svn status | awk '/\?/ {print $2}' | xargs rm -rf"):
499                 p = SilentPopen([svncommand], cwd=self.local, shell=True)
500                 if p.returncode != 0:
501                     raise VCSException("Svn reset ({0}) failed in {1}".format(svncommand, self.local))
502             if not self.refreshed:
503                 p = SilentPopen(['svn', 'update'] + self.userargs(), cwd=self.local)
504                 if p.returncode != 0:
505                     raise VCSException("Svn update failed")
506                 self.refreshed = True
507
508         revargs = list(['-r', rev] if rev else [])
509         p = SilentPopen(['svn', 'update', '--force'] + revargs + self.userargs(), cwd=self.local)
510         if p.returncode != 0:
511             raise VCSException("Svn update failed")
512
513     def getref(self):
514         p = SilentPopen(['svn', 'info'], cwd=self.local)
515         for line in p.stdout.splitlines():
516             if line and line.startswith('Last Changed Rev: '):
517                 return line[18:]
518         return None
519
520 class vcs_hg(vcs):
521
522     def repotype(self):
523         return 'hg'
524
525     def gotorevisionx(self, rev):
526         if not os.path.exists(self.local):
527             p = SilentPopen(['hg', 'clone', self.remote, self.local])
528             if p.returncode != 0:
529                 raise VCSException("Hg clone failed")
530         else:
531             p = SilentPopen(['hg status -uS | xargs rm -rf'], cwd=self.local, shell=True)
532             if p.returncode != 0:
533                 raise VCSException("Hg clean failed")
534             if not self.refreshed:
535                 p = SilentPopen(['hg', 'pull'], cwd=self.local)
536                 if p.returncode != 0:
537                     raise VCSException("Hg pull failed")
538                 self.refreshed = True
539
540         rev = str(rev if rev else 'default')
541         if not rev:
542             return
543         p = SilentPopen(['hg', 'update', '-C', rev], cwd=self.local)
544         if p.returncode != 0:
545             raise VCSException("Hg checkout failed")
546         p = SilentPopen(['hg', 'purge', '--all'], cwd=self.local)
547         # Also delete untracked files, we have to enable purge extension for that:
548         if "'purge' is provided by the following extension" in p.stdout:
549             with open(self.local+"/.hg/hgrc", "a") as myfile:
550                 myfile.write("\n[extensions]\nhgext.purge=\n")
551             p = SilentPopen(['hg', 'purge', '--all'], cwd=self.local)
552             if p.returncode != 0:
553                 raise VCSException("HG purge failed")
554         elif p.returncode != 0:
555             raise VCSException("HG purge failed")
556
557     def gettags(self):
558         p = SilentPopen(['hg', 'tags', '-q'], cwd=self.local)
559         return p.stdout.splitlines()[1:]
560
561
562 class vcs_bzr(vcs):
563
564     def repotype(self):
565         return 'bzr'
566
567     def gotorevisionx(self, rev):
568         if not os.path.exists(self.local):
569             p = SilentPopen(['bzr', 'branch', self.remote, self.local])
570             if p.returncode != 0:
571                 raise VCSException("Bzr branch failed")
572         else:
573             p = SilentPopen(['bzr', 'clean-tree', '--force', '--unknown', '--ignored'], cwd=self.local)
574             if p.returncode != 0:
575                 raise VCSException("Bzr revert failed")
576             if not self.refreshed:
577                 p = SilentPopen(['bzr', 'pull'], cwd=self.local)
578                 if p.returncode != 0:
579                     raise VCSException("Bzr update failed")
580                 self.refreshed = True
581
582         revargs = list(['-r', rev] if rev else [])
583         p = SilentPopen(['bzr', 'revert'] + revargs, cwd=self.local)
584         if p.returncode != 0:
585             raise VCSException("Bzr revert failed")
586
587     def gettags(self):
588         p = SilentPopen(['bzr', 'tags'], cwd=self.local)
589         return [tag.split('   ')[0].strip() for tag in
590                 p.stdout.splitlines()]
591
592 def retrieve_string(app_dir, string, xmlfiles=None):
593
594     res_dir = os.path.join(app_dir, 'res')
595
596     if xmlfiles is None:
597         xmlfiles = []
598         for r,d,f in os.walk(res_dir):
599             if r.endswith('/values'):
600                 xmlfiles += [os.path.join(r,x) for x in f if x.endswith('.xml')]
601
602     string_search = None
603     if string.startswith('@string/'):
604         string_search = re.compile(r'.*"'+string[8:]+'".*?>([^<]+?)<.*').search
605     elif string.startswith('&') and string.endswith(';'):
606         string_search = re.compile(r'.*<!ENTITY.*'+string[1:-1]+'.*?"([^"]+?)".*>').search
607
608     if string_search is not None:
609         for xmlfile in xmlfiles:
610             for line in file(xmlfile):
611                 matches = string_search(line)
612                 if matches:
613                     return retrieve_string(app_dir, matches.group(1), xmlfiles)
614
615     return string.replace("\\'","'")
616
617 # Return list of existing files that will be used to find the highest vercode
618 def manifest_paths(app_dir, flavour):
619
620     possible_manifests = [ os.path.join(app_dir, 'AndroidManifest.xml'),
621             os.path.join(app_dir, 'src', 'main', 'AndroidManifest.xml'),
622             os.path.join(app_dir, 'build.gradle') ]
623
624     if flavour:
625         possible_manifests.append(
626                 os.path.join(app_dir, 'src', flavour, 'AndroidManifest.xml'))
627
628     return [path for path in possible_manifests if os.path.isfile(path)]
629
630 # Retrieve the package name
631 def fetch_real_name(app_dir, flavour):
632     app_search = re.compile(r'.*<application.*').search
633     name_search = re.compile(r'.*android:label="([^"]+)".*').search
634     app_found = False
635     for f in manifest_paths(app_dir, flavour):
636         if not has_extension(f, 'xml'):
637             continue
638         logging.debug("fetch_real_name: Checking manifest at " + f)
639         for line in file(f):
640             if not app_found:
641                 if app_search(line):
642                     app_found = True
643             if app_found:
644                 matches = name_search(line)
645                 if matches:
646                     stringname = matches.group(1)
647                     logging.debug("fetch_real_name: using string " + stringname)
648                     return retrieve_string(app_dir, stringname).strip()
649     return ''
650
651 # Retrieve the version name
652 def version_name(original, app_dir, flavour):
653     for f in manifest_paths(app_dir, flavour):
654         if not has_extension(f, 'xml'):
655             continue
656         string = retrieve_string(app_dir, original)
657         if string:
658             return string
659     return original
660
661 def get_library_references(root_dir):
662     libraries = []
663     proppath = os.path.join(root_dir, 'project.properties')
664     if not os.path.isfile(proppath):
665         return libraries
666     with open(proppath) as f:
667         for line in f.readlines():
668             if not line.startswith('android.library.reference.'):
669                 continue
670             path = line.split('=')[1].strip()
671             relpath = os.path.join(root_dir, path)
672             if not os.path.isdir(relpath):
673                 continue
674             logging.info("Found subproject at %s" % path)
675             libraries.append(path)
676     return libraries
677
678 def ant_subprojects(root_dir):
679     subprojects = get_library_references(root_dir)
680     for subpath in subprojects:
681         subrelpath = os.path.join(root_dir, subpath)
682         for p in get_library_references(subrelpath):
683             relp = os.path.normpath(os.path.join(subpath,p))
684             if relp not in subprojects:
685                 subprojects.insert(0, relp)
686     return subprojects
687
688 def remove_debuggable_flags(root_dir):
689     # Remove forced debuggable flags
690     logging.info("Removing debuggable flags")
691     for root, dirs, files in os.walk(root_dir):
692         if 'AndroidManifest.xml' in files:
693             path = os.path.join(root, 'AndroidManifest.xml')
694             p = FDroidPopen(['sed','-i', 's/android:debuggable="[^"]*"//g', path])
695             if p.returncode != 0:
696                 raise BuildException("Failed to remove debuggable flags of %s" % path)
697
698 # Extract some information from the AndroidManifest.xml at the given path.
699 # Returns (version, vercode, package), any or all of which might be None.
700 # All values returned are strings.
701 def parse_androidmanifests(paths):
702
703     if not paths:
704         return (None, None, None)
705
706     vcsearch = re.compile(r'.*:versionCode="([0-9]+?)".*').search
707     vnsearch = re.compile(r'.*:versionName="([^"]+?)".*').search
708     psearch = re.compile(r'.*package="([^"]+)".*').search
709
710     vcsearch_g = re.compile(r'.*versionCode[ ]*[=]*[ ]*["\']*([0-9]+)["\']*').search
711     vnsearch_g = re.compile(r'.*versionName[ ]*[=]*[ ]*(["\'])((?:(?=(\\?))\3.)*?)\1.*').search
712     psearch_g = re.compile(r'.*packageName[ ]*[=]*[ ]*["\']([^"]+)["\'].*').search
713
714     max_version = None
715     max_vercode = None
716     max_package = None
717
718     for path in paths:
719
720         gradle = has_extension(path, 'gradle')
721         version = None
722         vercode = None
723         # Remember package name, may be defined separately from version+vercode
724         package = max_package
725
726         for line in file(path):
727             if not package:
728                 if gradle:
729                     matches = psearch_g(line)
730                 else:
731                     matches = psearch(line)
732                 if matches:
733                     package = matches.group(1)
734             if not version:
735                 if gradle:
736                     matches = vnsearch_g(line)
737                 else:
738                     matches = vnsearch(line)
739                 if matches:
740                     version = matches.group(2 if gradle else 1)
741             if not vercode:
742                 if gradle:
743                     matches = vcsearch_g(line)
744                 else:
745                     matches = vcsearch(line)
746                 if matches:
747                     vercode = matches.group(1)
748
749         # Better some package name than nothing
750         if max_package is None:
751             max_package = package
752
753         if max_vercode is None or (vercode is not None and vercode > max_vercode):
754             max_version = version
755             max_vercode = vercode
756             max_package = package
757
758     if max_version is None:
759         max_version = "Unknown"
760
761     return (max_version, max_vercode, max_package)
762
763 class BuildException(Exception):
764     def __init__(self, value, detail = None):
765         self.value = value
766         self.detail = detail
767
768     def get_wikitext(self):
769         ret = repr(self.value) + "\n"
770         if self.detail:
771             ret += "=detail=\n"
772             ret += "<pre>\n"
773             ret += str(self.detail)
774             ret += "</pre>\n"
775         return ret
776
777     def __str__(self):
778         ret = repr(self.value)
779         if self.detail:
780             ret += "\n==== detail begin ====\n%s\n==== detail end ====" % self.detail.strip()
781         return ret
782
783 class VCSException(Exception):
784     def __init__(self, value):
785         self.value = value
786
787     def __str__(self):
788         return repr(self.value)
789
790 # Get the specified source library.
791 # Returns the path to it. Normally this is the path to be used when referencing
792 # it, which may be a subdirectory of the actual project. If you want the base
793 # directory of the project, pass 'basepath=True'.
794 def getsrclib(spec, srclib_dir, srclibpaths=[], subdir=None,
795         basepath=False, raw=False, prepare=True, preponly=False):
796
797     number = None
798     subdir = None
799     if raw:
800         name = spec
801         ref = None
802     else:
803         name, ref = spec.split('@')
804         if ':' in name:
805             number, name = name.split(':', 1)
806         if '/' in name:
807             name, subdir = name.split('/',1)
808
809     srclib_path = os.path.join('srclibs', name + ".txt")
810
811     if not os.path.exists(srclib_path):
812         raise BuildException('srclib ' + name + ' not found.')
813
814     srclib = metadata.parse_srclib(srclib_path)
815
816     sdir = os.path.join(srclib_dir, name)
817
818     if not preponly:
819         vcs = getvcs(srclib["Repo Type"], srclib["Repo"], sdir)
820         vcs.srclib = (name, number, sdir)
821         if ref:
822             vcs.gotorevision(ref)
823
824         if raw:
825             return vcs
826
827     libdir = None
828     if subdir:
829         libdir = os.path.join(sdir, subdir)
830     elif srclib["Subdir"]:
831         for subdir in srclib["Subdir"]:
832             libdir_candidate = os.path.join(sdir, subdir)
833             if os.path.exists(libdir_candidate):
834                 libdir = libdir_candidate
835                 break
836
837     if libdir is None:
838         libdir = sdir
839
840     if srclib["Srclibs"]:
841         for n,lib in enumerate(srclib["Srclibs"].replace(';',',').split(',')):
842             s_tuple = None
843             for t in srclibpaths:
844                 if t[0] == lib:
845                     s_tuple = t
846                     break
847             if s_tuple is None:
848                 raise BuildException('Missing recursive srclib %s for %s' % (
849                     lib, name))
850             place_srclib(libdir, n, s_tuple[2])
851
852     remove_signing_keys(sdir)
853     remove_debuggable_flags(sdir)
854
855     if prepare:
856
857         if srclib["Prepare"]:
858             cmd = replace_config_vars(srclib["Prepare"])
859
860             p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=libdir)
861             if p.returncode != 0:
862                 raise BuildException("Error running prepare command for srclib %s"
863                         % name, p.stdout)
864
865     if basepath:
866         libdir = sdir
867
868     return (name, number, libdir)
869
870
871 # Prepare the source code for a particular build
872 #  'vcs'         - the appropriate vcs object for the application
873 #  'app'         - the application details from the metadata
874 #  'build'       - the build details from the metadata
875 #  'build_dir'   - the path to the build directory, usually
876 #                   'build/app.id'
877 #  'srclib_dir'  - the path to the source libraries directory, usually
878 #                   'build/srclib'
879 #  'extlib_dir'  - the path to the external libraries directory, usually
880 #                   'build/extlib'
881 # Returns the (root, srclibpaths) where:
882 #   'root' is the root directory, which may be the same as 'build_dir' or may
883 #          be a subdirectory of it.
884 #   'srclibpaths' is information on the srclibs being used
885 def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=False):
886
887     # Optionally, the actual app source can be in a subdirectory
888     if 'subdir' in build:
889         root_dir = os.path.join(build_dir, build['subdir'])
890     else:
891         root_dir = build_dir
892
893     # Get a working copy of the right revision
894     logging.info("Getting source for revision " + build['commit'])
895     vcs.gotorevision(build['commit'])
896
897     # Initialise submodules if requred
898     if build['submodules']:
899         logging.info("Initialising submodules")
900         vcs.initsubmodules()
901
902     # Check that a subdir (if we're using one) exists. This has to happen
903     # after the checkout, since it might not exist elsewhere
904     if not os.path.exists(root_dir):
905         raise BuildException('Missing subdir ' + root_dir)
906
907     # Run an init command if one is required
908     if 'init' in build:
909         cmd = replace_config_vars(build['init'])
910         logging.info("Running 'init' commands in %s" % root_dir)
911
912         p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
913         if p.returncode != 0:
914             raise BuildException("Error running init command for %s:%s" %
915                     (app['id'], build['version']), p.stdout)
916
917     # Apply patches if any
918     if 'patch' in build:
919         for patch in build['patch']:
920             patch = patch.strip()
921             logging.info("Applying " + patch)
922             patch_path = os.path.join('metadata', app['id'], patch)
923             p = FDroidPopen(['patch', '-p1', '-i', os.path.abspath(patch_path)], cwd=build_dir)
924             if p.returncode != 0:
925                 raise BuildException("Failed to apply patch %s" % patch_path)
926
927     # Get required source libraries
928     srclibpaths = []
929     if 'srclibs' in build:
930         logging.info("Collecting source libraries")
931         for lib in build['srclibs']:
932             srclibpaths.append(getsrclib(lib, srclib_dir, srclibpaths,
933                 preponly=onserver))
934
935     for name, number, libpath in srclibpaths:
936         place_srclib(root_dir, int(number) if number else None, libpath)
937
938     basesrclib = vcs.getsrclib()
939     # If one was used for the main source, add that too.
940     if basesrclib:
941         srclibpaths.append(basesrclib)
942
943     # Update the local.properties file
944     localprops = [ os.path.join(build_dir, 'local.properties') ]
945     if 'subdir' in build:
946         localprops += [ os.path.join(root_dir, 'local.properties') ]
947     for path in localprops:
948         if not os.path.isfile(path):
949             continue
950         logging.info("Updating properties file at %s" % path)
951         f = open(path, 'r')
952         props = f.read()
953         f.close()
954         props += '\n'
955         # Fix old-fashioned 'sdk-location' by copying
956         # from sdk.dir, if necessary
957         if build['oldsdkloc']:
958             sdkloc = re.match(r".*^sdk.dir=(\S+)$.*", props,
959                 re.S|re.M).group(1)
960             props += "sdk-location=%s\n" % sdkloc
961         else:
962             props += "sdk.dir=%s\n" % config['sdk_path']
963             props += "sdk-location=%s\n" % config['sdk_path']
964         if 'ndk_path' in config:
965             # Add ndk location
966             props += "ndk.dir=%s\n" % config['ndk_path']
967             props += "ndk-location=%s\n" % config['ndk_path']
968         # Add java.encoding if necessary
969         if 'encoding' in build:
970             props += "java.encoding=%s\n" % build['encoding']
971         f = open(path, 'w')
972         f.write(props)
973         f.close()
974
975     flavour = None
976     if build['type'] == 'gradle':
977         flavour = build['gradle'].split('@')[0]
978         if flavour in ['main', 'yes', '']:
979             flavour = None
980
981         if 'target' in build:
982             n = build["target"].split('-')[1]
983             FDroidPopen(['sed', '-i',
984                 's@compileSdkVersion[ ]*[0-9]*@compileSdkVersion '+n+'@g',
985                 'build.gradle'], cwd=root_dir)
986             if '@' in build['gradle']:
987                 gradle_dir = os.path.join(root_dir, build['gradle'].split('@',1)[1])
988                 gradle_dir = os.path.normpath(gradle_dir)
989                 FDroidPopen(['sed', '-i',
990                     's@compileSdkVersion[ ]*[0-9]*@compileSdkVersion '+n+'@g',
991                     'build.gradle'], cwd=gradle_dir)
992
993     # Remove forced debuggable flags
994     remove_debuggable_flags(root_dir)
995
996     # Insert version code and number into the manifest if necessary
997     if build['forceversion']:
998         logging.info("Changing the version name")
999         for path in manifest_paths(root_dir, flavour):
1000             if not os.path.isfile(path):
1001                 continue
1002             if has_extension(path, 'xml'):
1003                 p = SilentPopen(['sed', '-i',
1004                     's/android:versionName="[^"]*"/android:versionName="' + build['version'] + '"/g',
1005                     path])
1006                 if p.returncode != 0:
1007                     raise BuildException("Failed to amend manifest")
1008             elif has_extension(path, 'gradle'):
1009                 p = SilentPopen(['sed', '-i',
1010                     's/versionName[ ]*=[ ]*"[^"]*"/versionName = "' + build['version'] + '"/g',
1011                     path])
1012                 if p.returncode != 0:
1013                     raise BuildException("Failed to amend build.gradle")
1014     if build['forcevercode']:
1015         logging.info("Changing the version code")
1016         for path in manifest_paths(root_dir, flavour):
1017             if not os.path.isfile(path):
1018                 continue
1019             if has_extension(path, 'xml'):
1020                 p = SilentPopen(['sed', '-i',
1021                     's/android:versionCode="[^"]*"/android:versionCode="' + build['vercode'] + '"/g',
1022                     path])
1023                 if p.returncode != 0:
1024                     raise BuildException("Failed to amend manifest")
1025             elif has_extension(path, 'gradle'):
1026                 p = SilentPopen(['sed', '-i',
1027                     's/versionCode[ ]*=[ ]*[0-9]*/versionCode = ' + build['vercode'] + '/g',
1028                     path])
1029                 if p.returncode != 0:
1030                     raise BuildException("Failed to amend build.gradle")
1031
1032     # Delete unwanted files
1033     if 'rm' in build:
1034         for part in build['rm']:
1035             dest = os.path.join(build_dir, part)
1036             logging.info("Removing {0}".format(part))
1037             if os.path.lexists(dest):
1038                 if os.path.islink(dest):
1039                     SilentPopen(['unlink ' + dest], shell=True)
1040                 else:
1041                     SilentPopen(['rm -rf ' + dest], shell=True)
1042             else:
1043                 logging.info("...but it didn't exist")
1044
1045     remove_signing_keys(build_dir)
1046
1047     # Add required external libraries
1048     if 'extlibs' in build:
1049         logging.info("Collecting prebuilt libraries")
1050         libsdir = os.path.join(root_dir, 'libs')
1051         if not os.path.exists(libsdir):
1052             os.mkdir(libsdir)
1053         for lib in build['extlibs']:
1054             lib = lib.strip()
1055             logging.info("...installing extlib {0}".format(lib))
1056             libf = os.path.basename(lib)
1057             libsrc = os.path.join(extlib_dir, lib)
1058             if not os.path.exists(libsrc):
1059                 raise BuildException("Missing extlib file {0}".format(libsrc))
1060             shutil.copyfile(libsrc, os.path.join(libsdir, libf))
1061
1062     # Run a pre-build command if one is required
1063     if 'prebuild' in build:
1064         cmd = replace_config_vars(build['prebuild'])
1065
1066         # Substitute source library paths into prebuild commands
1067         for name, number, libpath in srclibpaths:
1068             libpath = os.path.relpath(libpath, root_dir)
1069             cmd = cmd.replace('$$' + name + '$$', libpath)
1070
1071         logging.info("Running 'prebuild' commands in %s" % root_dir)
1072
1073         p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
1074         if p.returncode != 0:
1075             raise BuildException("Error running prebuild command for %s:%s" %
1076                     (app['id'], build['version']), p.stdout)
1077
1078     updatemode = build.get('update', ['auto'])
1079     # Generate (or update) the ant build file, build.xml...
1080     if updatemode != ['no'] and build['type'] == 'ant':
1081         parms = [os.path.join(config['sdk_path'], 'tools', 'android'), 'update']
1082         lparms = parms + ['lib-project']
1083         parms = parms + ['project']
1084
1085         if 'target' in build and build['target']:
1086             parms += ['-t', build['target']]
1087             lparms += ['-t', build['target']]
1088         if updatemode == ['auto']:
1089             update_dirs = ant_subprojects(root_dir) + ['.']
1090         else:
1091             update_dirs = updatemode
1092
1093         for d in update_dirs:
1094             subdir = os.path.join(root_dir, d)
1095             if d == '.':
1096                 print("Updating main project")
1097                 cmd = parms + ['-p', d]
1098             else:
1099                 print("Updating subproject %s" % d)
1100                 cmd = lparms + ['-p', d]
1101             p = FDroidPopen(cmd, cwd=root_dir)
1102             # Check to see whether an error was returned without a proper exit
1103             # code (this is the case for the 'no target set or target invalid'
1104             # error)
1105             if p.returncode != 0 or p.stdout.startswith("Error: "):
1106                 raise BuildException("Failed to update project at %s" % d, p.stdout)
1107             # Clean update dirs via ant
1108             if d != '.':
1109                 logging.info("Cleaning subproject %s" % d)
1110                 p = FDroidPopen(['ant', 'clean'], cwd=subdir)
1111
1112     return (root_dir, srclibpaths)
1113
1114 # Scan the source code in the given directory (and all subdirectories)
1115 # and return a list of potential problems.
1116 def scan_source(build_dir, root_dir, thisbuild):
1117
1118     problems = []
1119
1120     # Common known non-free blobs (always lower case):
1121     usual_suspects = ['flurryagent',
1122                       'paypal_mpl',
1123                       'libgoogleanalytics',
1124                       'admob-sdk-android',
1125                       'googleadview',
1126                       'googleadmobadssdk',
1127                       'google-play-services',
1128                       'crittercism',
1129                       'heyzap',
1130                       'jpct-ae',
1131                       'youtubeandroidplayerapi',
1132                       'bugsense',
1133                       'crashlytics',
1134                       'ouya-sdk']
1135
1136     def getpaths(field):
1137         paths = []
1138         if field not in thisbuild:
1139             return paths
1140         for p in thisbuild[field]:
1141             p = p.strip()
1142             if p == '.':
1143                 p = '/'
1144             elif p.startswith('./'):
1145                 p = p[1:]
1146             elif not p.startswith('/'):
1147                 p = '/' + p;
1148             if p not in paths:
1149                 paths.append(p)
1150         return paths
1151
1152     scanignore = getpaths('scanignore')
1153     scandelete = getpaths('scandelete')
1154
1155     try:
1156         ms = magic.open(magic.MIME_TYPE)
1157         ms.load()
1158     except AttributeError:
1159         ms = None
1160
1161     def toignore(fd):
1162         for i in scanignore:
1163             if fd.startswith(i):
1164                 return True
1165         return False
1166
1167     def todelete(fd):
1168         for i in scandelete:
1169             if fd.startswith(i):
1170                 return True
1171         return False
1172
1173     def removeproblem(what, fd, fp):
1174         logging.info('Removing %s at %s' % (what, fd))
1175         os.remove(fp)
1176
1177     def handleproblem(what, fd, fp):
1178         if todelete(fd):
1179             removeproblem(what, fd, fp)
1180         else:
1181             problems.append('Found %s at %s' % (what, fd))
1182
1183     def warnproblem(what, fd, fp):
1184         logging.info('Warning: Found %s at %s' % (what, fd))
1185
1186     def insidedir(path, dirname):
1187         return path.endswith('/%s' % dirname) or '/%s/' % dirname in path
1188
1189     # Iterate through all files in the source code
1190     for r,d,f in os.walk(build_dir):
1191
1192         if any(insidedir(r, igndir) for igndir in ('.hg', '.git', '.svn')):
1193             continue
1194
1195         for curfile in f:
1196
1197             # Path (relative) to the file
1198             fp = os.path.join(r, curfile)
1199             fd = fp[len(build_dir):]
1200
1201             # Check if this file has been explicitly excluded from scanning
1202             if toignore(fd):
1203                 continue
1204
1205             for suspect in usual_suspects:
1206                 if suspect in curfile.lower():
1207                     handleproblem('usual supect', fd, fp)
1208
1209             mime = magic.from_file(fp, mime=True) if ms is None else ms.file(fp)
1210             if mime == 'application/x-sharedlib':
1211                 handleproblem('shared library', fd, fp)
1212             elif mime == 'application/x-archive':
1213                 handleproblem('static library', fd, fp)
1214             elif mime == 'application/x-executable':
1215                 handleproblem('binary executable', fd, fp)
1216             elif mime == 'application/x-java-applet':
1217                 handleproblem('Java compiled class', fd, fp)
1218             elif mime == 'application/jar' and has_extension(fp, 'apk'):
1219                 removeproblem('APK file', fd, fp)
1220             elif mime == 'application/jar' or (
1221                     mime == 'application/zip' and has_extension(fp, 'jar')):
1222                 warnproblem('JAR file', fd, fp)
1223             elif mime == 'application/zip':
1224                 warnproblem('ZIP file', fd, fp)
1225
1226             elif has_extension(fp, 'java'):
1227                 for line in file(fp):
1228                     if 'DexClassLoader' in line:
1229                         handleproblem('DexClassLoader', fd, fp)
1230                         break
1231     if ms is not None:
1232         ms.close()
1233
1234     # Presence of a jni directory without buildjni=yes might
1235     # indicate a problem (if it's not a problem, explicitly use
1236     # buildjni=no to bypass this check)
1237     if (os.path.exists(os.path.join(root_dir, 'jni')) and
1238             thisbuild.get('buildjni') is None):
1239         msg = 'Found jni directory, but buildjni is not enabled'
1240         problems.append(msg)
1241
1242     return problems
1243
1244
1245 class KnownApks:
1246
1247     def __init__(self):
1248         self.path = os.path.join('stats', 'known_apks.txt')
1249         self.apks = {}
1250         if os.path.exists(self.path):
1251             for line in file( self.path):
1252                 t = line.rstrip().split(' ')
1253                 if len(t) == 2:
1254                     self.apks[t[0]] = (t[1], None)
1255                 else:
1256                     self.apks[t[0]] = (t[1], time.strptime(t[2], '%Y-%m-%d'))
1257         self.changed = False
1258
1259     def writeifchanged(self):
1260         if self.changed:
1261             if not os.path.exists('stats'):
1262                 os.mkdir('stats')
1263             f = open(self.path, 'w')
1264             lst = []
1265             for apk, app in self.apks.iteritems():
1266                 appid, added = app
1267                 line = apk + ' ' + appid
1268                 if added:
1269                     line += ' ' + time.strftime('%Y-%m-%d', added)
1270                 lst.append(line)
1271             for line in sorted(lst):
1272                 f.write(line + '\n')
1273             f.close()
1274
1275     # Record an apk (if it's new, otherwise does nothing)
1276     # Returns the date it was added.
1277     def recordapk(self, apk, app):
1278         if not apk in self.apks:
1279             self.apks[apk] = (app, time.gmtime(time.time()))
1280             self.changed = True
1281         _, added = self.apks[apk]
1282         return added
1283
1284     # Look up information - given the 'apkname', returns (app id, date added/None).
1285     # Or returns None for an unknown apk.
1286     def getapp(self, apkname):
1287         if apkname in self.apks:
1288             return self.apks[apkname]
1289         return None
1290
1291     # Get the most recent 'num' apps added to the repo, as a list of package ids
1292     # with the most recent first.
1293     def getlatest(self, num):
1294         apps = {}
1295         for apk, app in self.apks.iteritems():
1296             appid, added = app
1297             if added:
1298                 if appid in apps:
1299                     if apps[appid] > added:
1300                         apps[appid] = added
1301                 else:
1302                     apps[appid] = added
1303         sortedapps = sorted(apps.iteritems(), key=operator.itemgetter(1))[-num:]
1304         lst = [app for app,added in sortedapps]
1305         lst.reverse()
1306         return lst
1307
1308 def isApkDebuggable(apkfile, config):
1309     """Returns True if the given apk file is debuggable
1310
1311     :param apkfile: full path to the apk to check"""
1312
1313     p = SilentPopen([os.path.join(config['sdk_path'],
1314         'build-tools', config['build_tools'], 'aapt'),
1315         'dump', 'xmltree', apkfile, 'AndroidManifest.xml'])
1316     if p.returncode != 0:
1317         logging.critical("Failed to get apk manifest information")
1318         sys.exit(1)
1319     for line in p.stdout.splitlines():
1320         if 'android:debuggable' in line and not line.endswith('0x0'):
1321             return True
1322     return False
1323
1324
1325 class AsynchronousFileReader(threading.Thread):
1326     '''
1327     Helper class to implement asynchronous reading of a file
1328     in a separate thread. Pushes read lines on a queue to
1329     be consumed in another thread.
1330     '''
1331
1332     def __init__(self, fd, queue):
1333         assert isinstance(queue, Queue.Queue)
1334         assert callable(fd.readline)
1335         threading.Thread.__init__(self)
1336         self._fd = fd
1337         self._queue = queue
1338
1339     def run(self):
1340         '''The body of the tread: read lines and put them on the queue.'''
1341         for line in iter(self._fd.readline, ''):
1342             self._queue.put(line)
1343
1344     def eof(self):
1345         '''Check whether there is no more content to expect.'''
1346         return not self.is_alive() and self._queue.empty()
1347
1348 class PopenResult:
1349     returncode = None
1350     stdout = ''
1351
1352 def SilentPopen(commands, cwd=None, shell=False):
1353     """
1354     Run a command silently and capture the output.
1355
1356     :param commands: command and argument list like in subprocess.Popen
1357     :param cwd: optionally specifies a working directory
1358     :returns: A Popen object.
1359     """
1360
1361     if cwd:
1362         cwd = os.path.normpath(cwd)
1363         logging.debug("Directory: %s" % cwd)
1364     logging.debug("> %s" % ' '.join(commands))
1365
1366     result = PopenResult()
1367     p = subprocess.Popen(commands, cwd=cwd, shell=shell,
1368             stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
1369
1370     result.stdout = p.communicate()[0]
1371     result.returncode = p.returncode
1372     return result
1373
1374 def FDroidPopen(commands, cwd=None, shell=False):
1375     """
1376     Run a command and capture the possibly huge output.
1377
1378     :param commands: command and argument list like in subprocess.Popen
1379     :param cwd: optionally specifies a working directory
1380     :returns: A PopenResult.
1381     """
1382
1383     if cwd:
1384         cwd = os.path.normpath(cwd)
1385         logging.info("Directory: %s" % cwd)
1386     logging.info("> %s" % ' '.join(commands))
1387
1388     result = PopenResult()
1389     p = subprocess.Popen(commands, cwd=cwd, shell=shell,
1390             stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
1391
1392     stdout_queue = Queue.Queue()
1393     stdout_reader = AsynchronousFileReader(p.stdout, stdout_queue)
1394     stdout_reader.start()
1395
1396     # Check the queue for output (until there is no more to get)
1397     while not stdout_reader.eof():
1398         while not stdout_queue.empty():
1399             line = stdout_queue.get()
1400             if options.verbose:
1401                 # Output directly to console
1402                 sys.stdout.write(line)
1403                 sys.stdout.flush()
1404             result.stdout += line
1405
1406         time.sleep(0.1)
1407
1408     p.communicate()
1409     result.returncode = p.returncode
1410     return result
1411
1412 def remove_signing_keys(build_dir):
1413     comment = re.compile(r'[ ]*//')
1414     signing_configs = re.compile(r'^[\t ]*signingConfigs[ \t]*{[ \t]*$')
1415     line_matches = [
1416             re.compile(r'^[\t ]*signingConfig [^ ]*$'),
1417             re.compile(r'android.signingConfigs.'),
1418             re.compile(r'variant.outputFile = '),
1419             re.compile(r'.readLine\('),
1420     ]
1421     for root, dirs, files in os.walk(build_dir):
1422         if 'build.gradle' in files:
1423             path = os.path.join(root, 'build.gradle')
1424
1425             with open(path, "r") as o:
1426                 lines = o.readlines()
1427
1428             opened = 0
1429             with open(path, "w") as o:
1430                 for line in lines:
1431                     if comment.match(line):
1432                         continue
1433
1434                     if opened > 0:
1435                         opened += line.count('{')
1436                         opened -= line.count('}')
1437                         continue
1438
1439                     if signing_configs.match(line):
1440                         opened += 1
1441                         continue
1442
1443                     if any(s.match(line) for s in line_matches):
1444                         continue
1445
1446                     if opened == 0:
1447                         o.write(line)
1448
1449             logging.info("Cleaned build.gradle of keysigning configs at %s" % path)
1450
1451         for propfile in [
1452                 'project.properties',
1453                 'build.properties',
1454                 'default.properties',
1455                 'ant.properties',
1456                 ]:
1457             if propfile in files:
1458                 path = os.path.join(root, propfile)
1459
1460                 with open(path, "r") as o:
1461                     lines = o.readlines()
1462
1463                 with open(path, "w") as o:
1464                     for line in lines:
1465                         if line.startswith('key.store'):
1466                             continue
1467                         if line.startswith('key.alias'):
1468                             continue
1469                         o.write(line)
1470
1471                 logging.info("Cleaned %s of keysigning configs at %s" % (propfile,path))
1472
1473 def replace_config_vars(cmd):
1474     cmd = cmd.replace('$$SDK$$', config['sdk_path'])
1475     cmd = cmd.replace('$$NDK$$', config['ndk_path'])
1476     cmd = cmd.replace('$$MVN3$$', config['mvn3'])
1477     return cmd
1478
1479 def place_srclib(root_dir, number, libpath):
1480     if not number:
1481         return
1482     relpath = os.path.relpath(libpath, root_dir)
1483     proppath = os.path.join(root_dir, 'project.properties')
1484
1485     lines = []
1486     if os.path.isfile(proppath):
1487         with open(proppath, "r") as o:
1488             lines = o.readlines()
1489
1490     with open(proppath, "w") as o:
1491         placed = False
1492         for line in lines:
1493             if line.startswith('android.library.reference.%d=' % number):
1494                 o.write('android.library.reference.%d=%s\n' % (number,relpath))
1495                 placed = True
1496             else:
1497                 o.write(line)
1498         if not placed:
1499             o.write('android.library.reference.%d=%s\n' % (number,relpath))
1500