chiark / gitweb /
Check if purge extension is enabled before attempting to enable it in .hg/hgrc
[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         p = subprocess.Popen(['hg', 'purge', '--all'], stdout=subprocess.PIPE,
476                              cwd=self.local)
477         result = p.communicate()[0]
478         if "'purge' is provided by the following extension" in result:
479             #Also delete untracked files, we have to enable purge extension for that:
480             with open(self.local+"/.hg/hgrc", "a") as myfile:
481                            myfile.write("\n[extensions]\nhgext.purge=")
482             if subprocess.call(['hg', 'purge', '--all'],
483                            cwd=self.local) != 0:
484                                    raise VCSException("HG purge failed")
485         else:
486             raise VCSException("HG purge failed")
487
488     def gettags(self):
489         p = subprocess.Popen(['hg', 'tags', '-q'],
490                 stdout=subprocess.PIPE, cwd=self.local)
491         return p.communicate()[0].splitlines()[1:]
492
493
494 class vcs_bzr(vcs):
495
496     def repotype(self):
497         return 'bzr'
498
499     def gotorevisionx(self, rev):
500         if not os.path.exists(self.local):
501             if subprocess.call(['bzr', 'branch', self.remote, self.local]) != 0:
502                 raise VCSException("Bzr branch failed")
503         else:
504             if subprocess.call(['bzr', 'clean-tree', '--force',
505                     '--unknown', '--ignored'], cwd=self.local) != 0:
506                 raise VCSException("Bzr revert failed")
507             if not self.refreshed:
508                 if subprocess.call(['bzr', 'pull'],
509                         cwd=self.local) != 0:
510                     raise VCSException("Bzr update failed")
511                 self.refreshed = True
512
513         revargs = list(['-r', rev] if rev else [])
514         if subprocess.call(['bzr', 'revert'] + revargs,
515                 cwd=self.local) != 0:
516             raise VCSException("Bzr revert failed")
517
518     def gettags(self):
519         p = subprocess.Popen(['bzr', 'tags'],
520                 stdout=subprocess.PIPE, cwd=self.local)
521         return [tag.split('   ')[0].strip() for tag in
522                 p.communicate()[0].splitlines()]
523
524 def retrieve_string(xml_dir, string):
525     if string.startswith('@string/'):
526         string_search = re.compile(r'.*"'+string[8:]+'".*?>([^<]+?)<.*').search
527         for xmlfile in glob.glob(os.path.join(xml_dir, '*.xml')):
528             for line in file(xmlfile):
529                 matches = string_search(line)
530                 if matches:
531                     return retrieve_string(xml_dir, matches.group(1))
532     elif string.startswith('&') and string.endswith(';'):
533         string_search = re.compile(r'.*<!ENTITY.*'+string[1:-1]+'.*?"([^"]+?)".*>').search
534         for xmlfile in glob.glob(os.path.join(xml_dir, '*.xml')):
535             for line in file(xmlfile):
536                 matches = string_search(line)
537                 if matches:
538                     return retrieve_string(xml_dir, matches.group(1))
539
540     return string.replace("\\'","'")
541
542 # Return list of existing files that will be used to find the highest vercode
543 def manifest_paths(app_dir, flavour):
544
545     possible_manifests = [ os.path.join(app_dir, 'AndroidManifest.xml'),
546             os.path.join(app_dir, 'src', 'main', 'AndroidManifest.xml'),
547             os.path.join(app_dir, 'build.gradle') ]
548
549     if flavour:
550         possible_manifests.append(
551                 os.path.join(app_dir, 'src', flavour, 'AndroidManifest.xml'))
552     
553     return [path for path in possible_manifests if os.path.isfile(path)]
554
555 # Retrieve the package name
556 def fetch_real_name(app_dir, flavour):
557     app_search = re.compile(r'.*<application.*').search
558     name_search = re.compile(r'.*android:label="([^"]+)".*').search
559     app_found = False
560     for f in manifest_paths(app_dir, flavour):
561         if not f.endswith(".xml"):
562             continue
563         xml_dir = os.path.join(f[:-19], 'res', 'values')
564         for line in file(f):
565             if not app_found:
566                 if app_search(line):
567                     app_found = True
568             if app_found:
569                 matches = name_search(line)
570                 if matches:
571                     return retrieve_string(xml_dir, matches.group(1))
572     return ''
573
574 # Retrieve the version name
575 def version_name(original, app_dir, flavour):
576     for f in manifest_paths(app_dir, flavour):
577         if not f.endswith(".xml"):
578             continue
579         xml_dir = os.path.join(f[:-19], 'res', 'values')
580         string = retrieve_string(xml_dir, original)
581         if string:
582             return string
583     return original
584
585 def ant_subprojects(root_dir):
586     subprojects = []
587     proppath = os.path.join(root_dir, 'project.properties')
588     if not os.path.isfile(proppath):
589         return subprojects
590     with open(proppath) as f:
591         for line in f.readlines():
592             if not line.startswith('android.library.reference.'):
593                 continue
594             path = line.split('=')[1].strip()
595             relpath = os.path.join(root_dir, path)
596             if not os.path.isdir(relpath):
597                 continue
598             if options.verbose:
599                 print "Found subproject %s..." % path
600             subprojects.append(path)
601     return subprojects
602
603 # Extract some information from the AndroidManifest.xml at the given path.
604 # Returns (version, vercode, package), any or all of which might be None.
605 # All values returned are strings.
606 def parse_androidmanifests(paths):
607
608     if not paths:
609         return (None, None, None)
610
611     vcsearch = re.compile(r'.*android:versionCode="([0-9]+?)".*').search
612     vnsearch = re.compile(r'.*android:versionName="([^"]+?)".*').search
613     psearch = re.compile(r'.*package="([^"]+)".*').search
614
615     vcsearch_g = re.compile(r'.*versionCode[ =]*([0-9]+?)[^\d].*').search
616     vnsearch_g = re.compile(r'.*versionName[ =]*"([^"]+?)".*').search
617     psearch_g = re.compile(r'.*packageName[ =]*"([^"]+)".*').search
618
619     max_version = None
620     max_vercode = None
621     max_package = None
622
623     for path in paths:
624
625         gradle = path.endswith("gradle")
626         version = None
627         vercode = None
628         # Remember package name, may be defined separately from version+vercode
629         package = max_package
630
631         for line in file(path):
632             if not package:
633                 if gradle:
634                     matches = psearch_g(line)
635                 else:
636                     matches = psearch(line)
637                 if matches:
638                     package = matches.group(1)
639             if not version:
640                 if gradle:
641                     matches = vnsearch_g(line)
642                 else:
643                     matches = vnsearch(line)
644                 if matches:
645                     version = matches.group(1)
646             if not vercode:
647                 if gradle:
648                     matches = vcsearch_g(line)
649                 else:
650                     matches = vcsearch(line)
651                 if matches:
652                     vercode = matches.group(1)
653
654         # Better some package name than nothing
655         if max_package is None:
656             max_package = package
657
658         if max_vercode is None or (vercode is not None and vercode > max_vercode):
659             max_version = version
660             max_vercode = vercode
661             max_package = package
662
663     if max_version is None:
664         max_version = "Unknown"
665
666     return (max_version, max_vercode, max_package)
667
668 class BuildException(Exception):
669     def __init__(self, value, stdout = None, stderr = None):
670         self.value = value
671         self.stdout = stdout
672         self.stderr = stderr
673
674     def get_wikitext(self):
675         ret = repr(self.value) + "\n"
676         if self.stdout:
677             ret += "=stdout=\n"
678             ret += "<pre>\n"
679             ret += str(self.stdout)
680             ret += "</pre>\n"
681         if self.stderr:
682             ret += "=stderr=\n"
683             ret += "<pre>\n"
684             ret += str(self.stderr)
685             ret += "</pre>\n"
686         return ret
687
688     def __str__(self):
689         ret = repr(self.value)
690         if self.stdout:
691             ret += "\n==== stdout begin ====\n%s\n==== stdout end ====" % self.stdout.strip()
692         if self.stderr:
693             ret += "\n==== stderr begin ====\n%s\n==== stderr end ====" % self.stderr.strip()
694         return ret
695
696 class VCSException(Exception):
697     def __init__(self, value):
698         self.value = value
699
700     def __str__(self):
701         return repr(self.value)
702
703 # Get the specified source library.
704 # Returns the path to it. Normally this is the path to be used when referencing
705 # it, which may be a subdirectory of the actual project. If you want the base
706 # directory of the project, pass 'basepath=True'.
707 def getsrclib(spec, srclib_dir, srclibpaths=[], subdir=None, target=None,
708         basepath=False, raw=False, prepare=True, preponly=False):
709
710     number = None
711     subdir = None
712     if raw:
713         name = spec
714         ref = None
715     else:
716         name, ref = spec.split('@')
717         if ':' in name:
718             number, name = name.split(':', 1)
719         if '/' in name:
720             name, subdir = name.split('/',1)
721
722     srclib_path = os.path.join('srclibs', name + ".txt")
723
724     if not os.path.exists(srclib_path):
725         raise BuildException('srclib ' + name + ' not found.')
726
727     srclib = metadata.parse_srclib(srclib_path)
728
729     sdir = os.path.join(srclib_dir, name)
730
731     if not preponly:
732         vcs = getvcs(srclib["Repo Type"], srclib["Repo"], sdir)
733         vcs.srclib = (name, number, sdir)
734         if ref:
735             vcs.gotorevision(ref)
736
737         if raw:
738             return vcs
739
740     libdir = None
741     if subdir:
742         libdir = os.path.join(sdir, subdir)
743     elif srclib["Subdir"]:
744         for subdir in srclib["Subdir"]:
745             libdir_candidate = os.path.join(sdir, subdir)
746             if os.path.exists(libdir_candidate):
747                 libdir = libdir_candidate
748                 break
749
750     if libdir is None:
751         libdir = sdir
752
753     if srclib["Srclibs"]:
754         n=1
755         for lib in srclib["Srclibs"].split(','):
756             s_tuple = None
757             for t in srclibpaths:
758                 if t[0] == lib:
759                     s_tuple = t
760                     break
761             if s_tuple is None:
762                 raise BuildException('Missing recursive srclib %s for %s' % (
763                     lib, name))
764             place_srclib(libdir, n, s_tuple[2])
765             n+=1
766
767     if prepare:
768
769         if srclib["Prepare"]:
770             cmd = replace_config_vars(srclib["Prepare"])
771
772             p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=libdir)
773             if p.returncode != 0:
774                 raise BuildException("Error running prepare command for srclib %s"
775                         % name, p.stdout, p.stderr)
776         
777         if srclib["Update Project"] == "Yes":
778             print "Updating srclib %s at path %s" % (name, libdir)
779             cmd = [os.path.join(config['sdk_path'], 'tools', 'android'),
780                 'update', 'project', '-p', libdir]
781             if target:
782                 cmd += ['-t', target]
783             p = FDroidPopen(cmd)
784             # Check to see whether an error was returned without a proper exit
785             # code (this is the case for the 'no target set or target invalid'
786             # error)
787             if p.returncode != 0 or (p.stderr != "" and
788                     p.stderr.startswith("Error: ")):
789                 raise BuildException("Failed to update srclib project {0}"
790                         .format(name), p.stdout, p.stderr)
791
792             remove_signing_keys(libdir)
793
794     if basepath:
795         libdir = sdir
796
797     return (name, number, libdir)
798
799
800 # Prepare the source code for a particular build
801 #  'vcs'         - the appropriate vcs object for the application
802 #  'app'         - the application details from the metadata
803 #  'build'       - the build details from the metadata
804 #  'build_dir'   - the path to the build directory, usually
805 #                   'build/app.id'
806 #  'srclib_dir'  - the path to the source libraries directory, usually
807 #                   'build/srclib'
808 #  'extlib_dir'  - the path to the external libraries directory, usually
809 #                   'build/extlib'
810 # Returns the (root, srclibpaths) where:
811 #   'root' is the root directory, which may be the same as 'build_dir' or may
812 #          be a subdirectory of it.
813 #   'srclibpaths' is information on the srclibs being used
814 def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=False):
815
816     # Optionally, the actual app source can be in a subdirectory...
817     if 'subdir' in build:
818         root_dir = os.path.join(build_dir, build['subdir'])
819     else:
820         root_dir = build_dir
821
822     # Get a working copy of the right revision...
823     print "Getting source for revision " + build['commit']
824     vcs.gotorevision(build['commit'])
825
826     # Check that a subdir (if we're using one) exists. This has to happen
827     # after the checkout, since it might not exist elsewhere...
828     if not os.path.exists(root_dir):
829         raise BuildException('Missing subdir ' + root_dir)
830
831     # Initialise submodules if requred...
832     if build['submodules']:
833         if options.verbose:
834             print "Initialising submodules..."
835         vcs.initsubmodules()
836
837     # Run an init command if one is required...
838     if 'init' in build:
839         cmd = replace_config_vars(build['init'])
840         if options.verbose:
841             print "Running 'init' commands in %s" % root_dir
842
843         p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
844         if p.returncode != 0:
845             raise BuildException("Error running init command for %s:%s" %
846                     (app['id'], build['version']), p.stdout, p.stderr)
847
848     # Generate (or update) the ant build file, build.xml...
849     updatemode = build.get('update', 'auto')
850     if (updatemode != 'no'
851             and build.get('maven', 'no') == 'no'
852             and build.get('kivy', 'no') == 'no'
853             and build.get('gradle', 'no') == 'no'):
854         parms = [os.path.join(config['sdk_path'], 'tools', 'android'),
855                 'update', 'project']
856         if 'target' in build and build['target']:
857             parms += ['-t', build['target']]
858         update_dirs = None
859         if updatemode == 'auto':
860             update_dirs = ['.'] + ant_subprojects(root_dir)
861         else:
862             update_dirs = [d.strip() for d in updatemode.split(';')]
863         # Force build.xml update if necessary...
864         if updatemode == 'force' or 'target' in build:
865             if updatemode == 'force':
866                 update_dirs = ['.']
867             buildxml = os.path.join(root_dir, 'build.xml')
868             if os.path.exists(buildxml):
869                 print 'Force-removing old build.xml'
870                 os.remove(buildxml)
871
872         for d in update_dirs:
873             # Remove gen and bin dirs in libraries
874             # rid of them...
875             for baddir in [
876                     'gen', 'bin', 'obj', # ant
877                     'libs/armeabi-v7a', 'libs/armeabi', # jni
878                     'libs/mips', 'libs/x86']:
879                 badpath = os.path.join(root_dir, d, baddir)
880                 if os.path.exists(badpath):
881                     print "Removing '%s'" % badpath
882                     shutil.rmtree(badpath)
883             dparms = parms + ['-p', d]
884             if options.verbose:
885                 if d == '.':
886                     print "Updating main project..."
887                 else:
888                     print "Updating subproject %s..." % d
889             p = FDroidPopen(dparms, cwd=root_dir)
890             # Check to see whether an error was returned without a proper exit
891             # code (this is the case for the 'no target set or target invalid'
892             # error)
893             if p.returncode != 0 or (p.stderr != "" and
894                     p.stderr.startswith("Error: ")):
895                 raise BuildException("Failed to update project at %s" % d,
896                         p.stdout, p.stderr)
897
898     # Update the local.properties file...
899     localprops = [ os.path.join(build_dir, 'local.properties') ]
900     if 'subdir' in build:
901         localprops += [ os.path.join(root_dir, 'local.properties') ]
902     for path in localprops:
903         if not os.path.isfile(path):
904             continue
905         if options.verbose:
906             print "Updating properties file at %s" % path
907         f = open(path, 'r')
908         props = f.read()
909         f.close()
910         props += '\n'
911         # Fix old-fashioned 'sdk-location' by copying
912         # from sdk.dir, if necessary...
913         if build['oldsdkloc']:
914             sdkloc = re.match(r".*^sdk.dir=(\S+)$.*", props,
915                 re.S|re.M).group(1)
916             props += "sdk-location=%s\n" % sdkloc
917         else:
918             props += "sdk.dir=%s\n" % config['sdk_path']
919             props += "sdk-location=%s\n" % ['sdk_path']
920         # Add ndk location...
921         props += "ndk.dir=%s\n" % config['ndk_path']
922         props += "ndk-location=%s\n" % config['ndk_path']
923         # Add java.encoding if necessary...
924         if 'encoding' in build:
925             props += "java.encoding=%s\n" % build['encoding']
926         f = open(path, 'w')
927         f.write(props)
928         f.close()
929
930     flavour = None
931     if build.get('gradle', 'no') != 'no':
932         flavour = build['gradle'].split('@')[0]
933         if flavour in ['main', 'yes', '']:
934             flavour = None
935
936     # Remove forced debuggable flags
937     print "Removing debuggable flags..."
938     for path in manifest_paths(root_dir, flavour):
939         if not os.path.isfile(path):
940             continue
941         if subprocess.call(['sed','-i',
942             's/android:debuggable="[^"]*"//g', path]) != 0:
943             raise BuildException("Failed to remove debuggable flags")
944
945     # Insert version code and number into the manifest if necessary...
946     if build['forceversion']:
947         print "Changing the version name..."
948         for path in manifest_paths(root_dir, flavour):
949             if not os.path.isfile(path):
950                 continue
951             if path.endswith('.xml'):
952                 if subprocess.call(['sed','-i',
953                     's/android:versionName="[^"]*"/android:versionName="' + build['version'] + '"/g',
954                     path]) != 0:
955                     raise BuildException("Failed to amend manifest")
956             elif path.endswith('.gradle'):
957                 if subprocess.call(['sed','-i',
958                     's/versionName[ ]*=[ ]*"[^"]*"/versionName = "' + build['version'] + '"/g',
959                     path]) != 0:
960                     raise BuildException("Failed to amend build.gradle")
961     if build['forcevercode']:
962         print "Changing the version code..."
963         for path in manifest_paths(root_dir, flavour):
964             if not os.path.isfile(path):
965                 continue
966             if path.endswith('.xml'):
967                 if subprocess.call(['sed','-i',
968                     's/android:versionCode="[^"]*"/android:versionCode="' + build['vercode'] + '"/g',
969                     path]) != 0:
970                     raise BuildException("Failed to amend manifest")
971             elif path.endswith('.gradle'):
972                 if subprocess.call(['sed','-i',
973                     's/versionCode[ ]*=[ ]*[0-9]*/versionCode = ' + build['vercode'] + '/g',
974                     path]) != 0:
975                     raise BuildException("Failed to amend build.gradle")
976
977     # Delete unwanted files...
978     if 'rm' in build:
979         for part in build['rm'].split(';'):
980             dest = os.path.join(build_dir, part.strip())
981             rdest = os.path.abspath(dest)
982             if options.verbose:
983                 print "Removing {0}".format(rdest)
984             if not rdest.startswith(os.path.abspath(build_dir)):
985                 raise BuildException("rm for {1} is outside build root {0}".format(
986                     os.path.abspath(build_dir),os.path.abspath(dest)))
987             if rdest == os.path.abspath(build_dir):
988                 raise BuildException("rm removes whole build directory")
989             if os.path.lexists(rdest):
990                 if os.path.islink(rdest):
991                     subprocess.call('unlink ' + rdest, shell=True)
992                 else:
993                     subprocess.call('rm -rf ' + rdest, shell=True)
994             else:
995                 if options.verbose:
996                     print "...but it didn't exist"
997
998     # Fix apostrophes translation files if necessary...
999     if build['fixapos']:
1000         for root, dirs, files in os.walk(os.path.join(root_dir, 'res')):
1001             for filename in files:
1002                 if filename.endswith('.xml'):
1003                     if subprocess.call(['sed','-i','s@' +
1004                         r"\([^\\]\)'@\1\\'" +
1005                         '@g',
1006                         os.path.join(root, filename)]) != 0:
1007                         raise BuildException("Failed to amend " + filename)
1008
1009     # Fix translation files if necessary...
1010     if build['fixtrans']:
1011         for root, dirs, files in os.walk(os.path.join(root_dir, 'res')):
1012             for filename in files:
1013                 if filename.endswith('.xml'):
1014                     f = open(os.path.join(root, filename))
1015                     changed = False
1016                     outlines = []
1017                     for line in f:
1018                         num = 1
1019                         index = 0
1020                         oldline = line
1021                         while True:
1022                             index = line.find("%", index)
1023                             if index == -1:
1024                                 break
1025                             next = line[index+1:index+2]
1026                             if next == "s" or next == "d":
1027                                 line = (line[:index+1] +
1028                                         str(num) + "$" +
1029                                         line[index+1:])
1030                                 num += 1
1031                                 index += 3
1032                             else:
1033                                 index += 1
1034                         # We only want to insert the positional arguments
1035                         # when there is more than one argument...
1036                         if oldline != line:
1037                             if num > 2:
1038                                 changed = True
1039                             else:
1040                                 line = oldline
1041                         outlines.append(line)
1042                     f.close()
1043                     if changed:
1044                         f = open(os.path.join(root, filename), 'w')
1045                         f.writelines(outlines)
1046                         f.close()
1047
1048     remove_signing_keys(build_dir)
1049
1050     # Add required external libraries...
1051     if 'extlibs' in build:
1052         print "Collecting prebuilt libraries..."
1053         libsdir = os.path.join(root_dir, 'libs')
1054         if not os.path.exists(libsdir):
1055             os.mkdir(libsdir)
1056         for lib in build['extlibs'].split(';'):
1057             lib = lib.strip()
1058             if options.verbose:
1059                 print "...installing extlib {0}".format(lib)
1060             libf = os.path.basename(lib)
1061             libsrc = os.path.join(extlib_dir, lib)
1062             if not os.path.exists(libsrc):
1063                 raise BuildException("Missing extlib file {0}".format(libsrc))
1064             shutil.copyfile(libsrc, os.path.join(libsdir, libf))
1065
1066     # Get required source libraries...
1067     srclibpaths = []
1068     if 'srclibs' in build:
1069         target=build['target'] if 'target' in build else None
1070         print "Collecting source libraries..."
1071         for lib in build['srclibs'].split(';'):
1072             srclibpaths.append(getsrclib(lib, srclib_dir, srclibpaths,
1073                 target=target, preponly=onserver))
1074
1075     # Apply patches if any
1076     if 'patch' in build:
1077         for patch in build['patch'].split(';'):
1078             patch = patch.strip()
1079             print "Applying " + patch
1080             patch_path = os.path.join('metadata', app['id'], patch)
1081             if subprocess.call(['patch', '-p1',
1082                             '-i', os.path.abspath(patch_path)], cwd=build_dir) != 0:
1083                 raise BuildException("Failed to apply patch %s" % patch_path)
1084
1085     for name, number, libpath in srclibpaths:
1086         place_srclib(root_dir, int(number) if number else None, libpath)
1087
1088     basesrclib = vcs.getsrclib()
1089     # If one was used for the main source, add that too.
1090     if basesrclib:
1091         srclibpaths.append(basesrclib)
1092
1093     # Run a pre-build command if one is required...
1094     if 'prebuild' in build:
1095         cmd = replace_config_vars(build['prebuild'])
1096
1097         # Substitute source library paths into prebuild commands...
1098         for name, number, libpath in srclibpaths:
1099             libpath = os.path.relpath(libpath, root_dir)
1100             cmd = cmd.replace('$$' + name + '$$', libpath)
1101
1102         if options.verbose:
1103             print "Running 'prebuild' commands in %s" % root_dir
1104
1105         p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
1106         if p.returncode != 0:
1107             raise BuildException("Error running prebuild command for %s:%s" %
1108                     (app['id'], build['version']), p.stdout, p.stderr)
1109
1110     return (root_dir, srclibpaths)
1111
1112
1113 # Scan the source code in the given directory (and all subdirectories)
1114 # and return a list of potential problems.
1115 def scan_source(build_dir, root_dir, thisbuild):
1116
1117     problems = []
1118
1119     # Common known non-free blobs (always lower case):
1120     usual_suspects = ['flurryagent',
1121                       'paypal_mpl',
1122                       'libgoogleanalytics',
1123                       'admob-sdk-android',
1124                       'googleadview',
1125                       'googleadmobadssdk',
1126                       'google-play-services',
1127                       'crittercism',
1128                       'heyzap',
1129                       'jpct-ae',
1130                       'youtubeandroidplayerapi',
1131                       'bugsense',
1132                       'crashlytics',
1133                       'ouya-sdk']
1134
1135     def getpaths(field):
1136         paths = []
1137         if field not in thisbuild:
1138             return paths
1139         for p in thisbuild[field].split(';'):
1140             p = p.strip()
1141             if p == '.':
1142                 p = '/'
1143             elif p.startswith('./'):
1144                 p = p[1:]
1145             elif not p.startswith('/'):
1146                 p = '/' + p;
1147             if p not in paths:
1148                 paths.append(p)
1149         return paths
1150
1151     scanignore = getpaths('scanignore')
1152     scandelete = getpaths('scandelete')
1153
1154     ms = magic.open(magic.MIME_TYPE)
1155     ms.load()
1156
1157     def toignore(fd):
1158         for i in scanignore:
1159             if fd.startswith(i):
1160                 return True
1161         return False
1162
1163     def todelete(fd):
1164         for i in scandelete:
1165             if fd.startswith(i):
1166                 return True
1167         return False
1168
1169     def removeproblem(what, fd, fp):
1170         print 'Removing %s at %s' % (what, fd)
1171         os.remove(fp)
1172     
1173     def handleproblem(what, fd, fp):
1174         if todelete(fd):
1175             removeproblem(what, fd, fp)
1176         else:
1177             problems.append('Found %s at %s' % (what, fd))
1178
1179     # Iterate through all files in the source code...
1180     for r,d,f in os.walk(build_dir):
1181         for curfile in f:
1182
1183             if '/.hg' in r or '/.git' in r or '/.svn' in r:
1184                 continue
1185
1186             # Path (relative) to the file...
1187             fp = os.path.join(r, curfile)
1188             fd = fp[len(build_dir):]
1189
1190             # Check if this file has been explicitly excluded from scanning...
1191             if toignore(fd):
1192                 continue
1193
1194             for suspect in usual_suspects:
1195                 if suspect in curfile.lower():
1196                     handleproblem('usual supect', fd, fp)
1197
1198             mime = ms.file(fp)
1199             if mime == 'application/x-sharedlib':
1200                 handleproblem('shared library', fd, fp)
1201             elif mime == 'application/x-archive':
1202                 handleproblem('static library', fd, fp)
1203             elif mime == 'application/x-executable':
1204                 handleproblem('binary executable', fd, fp)
1205             elif mime == 'application/jar' and fp.endswith('.apk'):
1206                 removeproblem('APK file', fd, fp)
1207
1208             elif curfile.endswith('.java'):
1209                 for line in file(fp):
1210                     if 'DexClassLoader' in line:
1211                         handleproblem('DexClassLoader', fd, fp)
1212                         break
1213     ms.close()
1214
1215     # Presence of a jni directory without buildjni=yes might
1216     # indicate a problem... (if it's not a problem, explicitly use
1217     # buildjni=no to bypass this check)
1218     if (os.path.exists(os.path.join(root_dir, 'jni')) and 
1219             thisbuild.get('buildjni') is None):
1220         msg = 'Found jni directory, but buildjni is not enabled'
1221         problems.append(msg)
1222
1223     return problems
1224
1225
1226 class KnownApks:
1227
1228     def __init__(self):
1229         self.path = os.path.join('stats', 'known_apks.txt')
1230         self.apks = {}
1231         if os.path.exists(self.path):
1232             for line in file( self.path):
1233                 t = line.rstrip().split(' ')
1234                 if len(t) == 2:
1235                     self.apks[t[0]] = (t[1], None)
1236                 else:
1237                     self.apks[t[0]] = (t[1], time.strptime(t[2], '%Y-%m-%d'))
1238         self.changed = False
1239
1240     def writeifchanged(self):
1241         if self.changed:
1242             if not os.path.exists('stats'):
1243                 os.mkdir('stats')
1244             f = open(self.path, 'w')
1245             lst = []
1246             for apk, app in self.apks.iteritems():
1247                 appid, added = app
1248                 line = apk + ' ' + appid
1249                 if added:
1250                     line += ' ' + time.strftime('%Y-%m-%d', added)
1251                 lst.append(line)
1252             for line in sorted(lst):
1253                 f.write(line + '\n')
1254             f.close()
1255
1256     # Record an apk (if it's new, otherwise does nothing)
1257     # Returns the date it was added.
1258     def recordapk(self, apk, app):
1259         if not apk in self.apks:
1260             self.apks[apk] = (app, time.gmtime(time.time()))
1261             self.changed = True
1262         _, added = self.apks[apk]
1263         return added
1264
1265     # Look up information - given the 'apkname', returns (app id, date added/None).
1266     # Or returns None for an unknown apk.
1267     def getapp(self, apkname):
1268         if apkname in self.apks:
1269             return self.apks[apkname]
1270         return None
1271
1272     # Get the most recent 'num' apps added to the repo, as a list of package ids
1273     # with the most recent first.
1274     def getlatest(self, num):
1275         apps = {}
1276         for apk, app in self.apks.iteritems():
1277             appid, added = app
1278             if added:
1279                 if appid in apps:
1280                     if apps[appid] > added:
1281                         apps[appid] = added
1282                 else:
1283                     apps[appid] = added
1284         sortedapps = sorted(apps.iteritems(), key=operator.itemgetter(1))[-num:]
1285         lst = [app for app,added in sortedapps]
1286         lst.reverse()
1287         return lst
1288
1289 def isApkDebuggable(apkfile, config):
1290     """Returns True if the given apk file is debuggable
1291
1292     :param apkfile: full path to the apk to check"""
1293
1294     p = subprocess.Popen([os.path.join(config['sdk_path'],
1295         'build-tools', config['build_tools'], 'aapt'),
1296         'dump', 'xmltree', apkfile, 'AndroidManifest.xml'],
1297         stdout=subprocess.PIPE)
1298     output = p.communicate()[0]
1299     if p.returncode != 0:
1300         print "ERROR: Failed to get apk manifest information"
1301         sys.exit(1)
1302     for line in output.splitlines():
1303         if line.find('android:debuggable') != -1 and not line.endswith('0x0'):
1304             return True
1305     return False
1306
1307
1308 class AsynchronousFileReader(threading.Thread):
1309     '''
1310     Helper class to implement asynchronous reading of a file
1311     in a separate thread. Pushes read lines on a queue to
1312     be consumed in another thread.
1313     '''
1314  
1315     def __init__(self, fd, queue):
1316         assert isinstance(queue, Queue.Queue)
1317         assert callable(fd.readline)
1318         threading.Thread.__init__(self)
1319         self._fd = fd
1320         self._queue = queue
1321  
1322     def run(self):
1323         '''The body of the tread: read lines and put them on the queue.'''
1324         for line in iter(self._fd.readline, ''):
1325             self._queue.put(line)
1326  
1327     def eof(self):
1328         '''Check whether there is no more content to expect.'''
1329         return not self.is_alive() and self._queue.empty()
1330
1331 class PopenResult:
1332     returncode = None
1333     stdout = ''
1334     stderr = ''
1335     stdout_apk = ''
1336
1337 def FDroidPopen(commands, cwd=None):
1338     """
1339     Runs a command the FDroid way and returns return code and output
1340
1341     :param commands, cwd: like subprocess.Popen
1342     """
1343
1344     if options.verbose:
1345         if cwd:
1346             print "Directory: %s" % cwd
1347         print " > %s" % ' '.join(commands)
1348
1349     result = PopenResult()
1350     p = subprocess.Popen(commands, cwd=cwd,
1351             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1352     
1353     stdout_queue = Queue.Queue()
1354     stdout_reader = AsynchronousFileReader(p.stdout, stdout_queue)
1355     stdout_reader.start()
1356     stderr_queue = Queue.Queue()
1357     stderr_reader = AsynchronousFileReader(p.stderr, stderr_queue)
1358     stderr_reader.start()
1359     
1360     # Check the queues for output (until there is no more to get)
1361     while not stdout_reader.eof() or not stderr_reader.eof():
1362         # Show what we received from standard output
1363         while not stdout_queue.empty():
1364             line = stdout_queue.get()
1365             if options.verbose:
1366                 # Output directly to console
1367                 sys.stdout.write(line)
1368                 sys.stdout.flush()
1369             result.stdout += line
1370
1371         # Show what we received from standard error
1372         while not stderr_queue.empty():
1373             line = stderr_queue.get()
1374             if options.verbose:
1375                 # Output directly to console
1376                 sys.stderr.write(line)
1377                 sys.stderr.flush()
1378             result.stderr += line
1379         time.sleep(0.2)
1380
1381     p.communicate()
1382     result.returncode = p.returncode
1383     return result
1384
1385 def remove_signing_keys(build_dir):
1386     for root, dirs, files in os.walk(build_dir):
1387         if 'build.gradle' in files:
1388             path = os.path.join(root, 'build.gradle')
1389             changed = False
1390
1391             with open(path, "r") as o:
1392                 lines = o.readlines()
1393             
1394             opened = 0
1395             with open(path, "w") as o:
1396                 for line in lines:
1397                     if 'signingConfigs ' in line:
1398                         opened = 1
1399                         changed = True
1400                     elif opened > 0:
1401                         if '{' in line:
1402                             opened += 1
1403                         elif '}' in line:
1404                             opened -=1
1405                     elif any(s in line for s in (
1406                             ' signingConfig ',
1407                             'android.signingConfigs.',
1408                             'variant.outputFile = ',
1409                             '.readLine(')):
1410                         changed = True
1411                     else:
1412                         o.write(line)
1413
1414             if changed and options.verbose:
1415                 print "Cleaned build.gradle of keysigning configs at %s" % path
1416
1417         for propfile in ('build.properties', 'default.properties', 'ant.properties'):
1418             if propfile in files:
1419                 path = os.path.join(root, propfile)
1420                 changed = False
1421
1422                 with open(path, "r") as o:
1423                     lines = o.readlines()
1424
1425                 with open(path, "w") as o:
1426                     for line in lines:
1427                         if line.startswith('key.store'):
1428                             changed = True
1429                         else:
1430                             o.write(line)
1431
1432                 if changed and options.verbose:
1433                     print "Cleaned %s of keysigning configs at %s" % (propfile,path)
1434
1435 def replace_config_vars(cmd):
1436     cmd = cmd.replace('$$SDK$$', config['sdk_path'])
1437     cmd = cmd.replace('$$NDK$$', config['ndk_path'])
1438     cmd = cmd.replace('$$MVN3$$', config['mvn3'])
1439     return cmd
1440
1441 def place_srclib(root_dir, number, libpath):
1442     if not number:
1443         return
1444     relpath = os.path.relpath(libpath, root_dir)
1445     proppath = os.path.join(root_dir, 'project.properties')
1446
1447     with open(proppath, "r") as o:
1448         lines = o.readlines()
1449
1450     with open(proppath, "w") as o:
1451         placed = False
1452         for line in lines:
1453             if line.startswith('android.library.reference.%d=' % number):
1454                 o.write('android.library.reference.%d=%s\n' % (number,relpath))
1455                 placed = True
1456             else:
1457                 o.write(line)
1458         if not placed:
1459             o.write('android.library.reference.%d=%s\n' % (number,relpath))
1460