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