1 # -*- coding: utf-8 -*-
3 # common.py - part of the FDroid server tools
4 # Copyright (C) 2010-13, Ciaran Gultnieks, ciaran@ciarang.com
5 # Copyright (C) 2013-2014 Daniel Martà <mvdan@mvdan.cc>
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
33 from distutils.version import LooseVersion
43 'sdk_path': "$ANDROID_HOME",
44 'ndk_path': "$ANDROID_NDK",
45 'build_tools': "21.1.2",
49 'sync_from_local_copy_dir': False,
50 'make_current_version_link': True,
51 'current_version_name_source': 'Name',
52 'update_stats': False,
56 'stats_to_carbon': False,
58 'build_server_always': False,
59 'keystore': os.path.join("$HOME", '.local', 'share', 'fdroidserver', 'keystore.jks'),
60 'smartcardoptions': [],
66 'repo_url': "https://MyFirstFDroidRepo.org/fdroid/repo",
67 'repo_name': "My First FDroid Repo Demo",
68 'repo_icon': "fdroid-icon.png",
69 'repo_description': '''
70 This is a repository of apps to be used with FDroid. Applications in this
71 repository are either official binaries built by the original application
72 developers, or are binaries built from source by the admin of f-droid.org
73 using the tools on https://gitlab.com/u/fdroid.
79 def fill_config_defaults(thisconfig):
80 for k, v in default_config.items():
81 if k not in thisconfig:
84 # Expand paths (~users and $vars)
85 for k in ['sdk_path', 'ndk_path', 'ant', 'mvn3', 'gradle', 'keystore', 'repo_icon']:
88 v = os.path.expanduser(v)
89 v = os.path.expandvars(v)
92 thisconfig[k + '_orig'] = orig
95 def read_config(opts, config_file='config.py'):
96 """Read the repository config
98 The config is read from config_file, which is in the current directory when
99 any of the repo management commands are used.
101 global config, options, env
103 if config is not None:
105 if not os.path.isfile(config_file):
106 logging.critical("Missing config file - is this a repo directory?")
113 logging.debug("Reading %s" % config_file)
114 execfile(config_file, config)
116 # smartcardoptions must be a list since its command line args for Popen
117 if 'smartcardoptions' in config:
118 config['smartcardoptions'] = config['smartcardoptions'].split(' ')
119 elif 'keystore' in config and config['keystore'] == 'NONE':
120 # keystore='NONE' means use smartcard, these are required defaults
121 config['smartcardoptions'] = ['-storetype', 'PKCS11', '-providerName',
122 'SunPKCS11-OpenSC', '-providerClass',
123 'sun.security.pkcs11.SunPKCS11',
124 '-providerArg', 'opensc-fdroid.cfg']
126 if any(k in config for k in ["keystore", "keystorepass", "keypass"]):
127 st = os.stat(config_file)
128 if st.st_mode & stat.S_IRWXG or st.st_mode & stat.S_IRWXO:
129 logging.warn("unsafe permissions on {0} (should be 0600)!".format(config_file))
131 fill_config_defaults(config)
133 # There is no standard, so just set up the most common environment
136 for n in ['ANDROID_HOME', 'ANDROID_SDK']:
137 env[n] = config['sdk_path']
138 for n in ['ANDROID_NDK', 'NDK']:
139 env[n] = config['ndk_path']
141 for k in ["keystorepass", "keypass"]:
143 write_password_file(k)
145 for k in ["repo_description", "archive_description"]:
147 config[k] = clean_description(config[k])
149 if 'serverwebroot' in config:
150 if isinstance(config['serverwebroot'], basestring):
151 roots = [config['serverwebroot']]
152 elif all(isinstance(item, basestring) for item in config['serverwebroot']):
153 roots = config['serverwebroot']
155 raise TypeError('only accepts strings, lists, and tuples')
157 for rootstr in roots:
158 # since this is used with rsync, where trailing slashes have
159 # meaning, ensure there is always a trailing slash
160 if rootstr[-1] != '/':
162 rootlist.append(rootstr.replace('//', '/'))
163 config['serverwebroot'] = rootlist
168 def find_sdk_tools_cmd(cmd):
169 '''find a working path to a tool from the Android SDK'''
172 if config is not None and 'sdk_path' in config and os.path.exists(config['sdk_path']):
173 # try to find a working path to this command, in all the recent possible paths
174 if 'build_tools' in config:
175 build_tools = os.path.join(config['sdk_path'], 'build-tools')
176 # if 'build_tools' was manually set and exists, check only that one
177 configed_build_tools = os.path.join(build_tools, config['build_tools'])
178 if os.path.exists(configed_build_tools):
179 tooldirs.append(configed_build_tools)
181 # no configed version, so hunt known paths for it
182 for f in sorted(os.listdir(build_tools), reverse=True):
183 if os.path.isdir(os.path.join(build_tools, f)):
184 tooldirs.append(os.path.join(build_tools, f))
185 tooldirs.append(build_tools)
186 sdk_tools = os.path.join(config['sdk_path'], 'tools')
187 if os.path.exists(sdk_tools):
188 tooldirs.append(sdk_tools)
189 sdk_platform_tools = os.path.join(config['sdk_path'], 'platform-tools')
190 if os.path.exists(sdk_platform_tools):
191 tooldirs.append(sdk_platform_tools)
192 tooldirs.append('/usr/bin')
194 if os.path.isfile(os.path.join(d, cmd)):
195 return os.path.join(d, cmd)
196 # did not find the command, exit with error message
197 ensure_build_tools_exists(config)
200 def test_sdk_exists(thisconfig):
201 if 'sdk_path' not in thisconfig:
202 if 'aapt' in thisconfig and os.path.isfile(thisconfig['aapt']):
205 logging.error("'sdk_path' not set in config.py!")
207 if thisconfig['sdk_path'] == default_config['sdk_path']:
208 logging.error('No Android SDK found!')
209 logging.error('You can use ANDROID_HOME to set the path to your SDK, i.e.:')
210 logging.error('\texport ANDROID_HOME=/opt/android-sdk')
212 if not os.path.exists(thisconfig['sdk_path']):
213 logging.critical('Android SDK path "' + thisconfig['sdk_path'] + '" does not exist!')
215 if not os.path.isdir(thisconfig['sdk_path']):
216 logging.critical('Android SDK path "' + thisconfig['sdk_path'] + '" is not a directory!')
218 for d in ['build-tools', 'platform-tools', 'tools']:
219 if not os.path.isdir(os.path.join(thisconfig['sdk_path'], d)):
220 logging.critical('Android SDK path "%s" does not contain "%s/"!' % (
221 thisconfig['sdk_path'], d))
226 def ensure_build_tools_exists(thisconfig):
227 if not test_sdk_exists(thisconfig):
229 build_tools = os.path.join(thisconfig['sdk_path'], 'build-tools')
230 versioned_build_tools = os.path.join(build_tools, thisconfig['build_tools'])
231 if not os.path.isdir(versioned_build_tools):
232 logging.critical('Android Build Tools path "'
233 + versioned_build_tools + '" does not exist!')
237 def write_password_file(pwtype, password=None):
239 writes out passwords to a protected file instead of passing passwords as
240 command line argments
242 filename = '.fdroid.' + pwtype + '.txt'
243 fd = os.open(filename, os.O_CREAT | os.O_TRUNC | os.O_WRONLY, 0600)
245 os.write(fd, config[pwtype])
247 os.write(fd, password)
249 config[pwtype + 'file'] = filename
252 # Given the arguments in the form of multiple appid:[vc] strings, this returns
253 # a dictionary with the set of vercodes specified for each package.
254 def read_pkg_args(args, allow_vercodes=False):
261 if allow_vercodes and ':' in p:
262 package, vercode = p.split(':')
264 package, vercode = p, None
265 if package not in vercodes:
266 vercodes[package] = [vercode] if vercode else []
268 elif vercode and vercode not in vercodes[package]:
269 vercodes[package] += [vercode] if vercode else []
274 # On top of what read_pkg_args does, this returns the whole app metadata, but
275 # limiting the builds list to the builds matching the vercodes specified.
276 def read_app_args(args, allapps, allow_vercodes=False):
278 vercodes = read_pkg_args(args, allow_vercodes)
284 for appid, app in allapps.iteritems():
285 if appid in vercodes:
288 if len(apps) != len(vercodes):
291 logging.critical("No such package: %s" % p)
292 raise FDroidException("Found invalid app ids in arguments")
294 raise FDroidException("No packages specified")
297 for appid, app in apps.iteritems():
301 app['builds'] = [b for b in app['builds'] if b['vercode'] in vc]
302 if len(app['builds']) != len(vercodes[appid]):
304 allvcs = [b['vercode'] for b in app['builds']]
305 for v in vercodes[appid]:
307 logging.critical("No such vercode %s for app %s" % (v, appid))
310 raise FDroidException("Found invalid vercodes for some apps")
315 def has_extension(filename, extension):
316 name, ext = os.path.splitext(filename)
317 ext = ext.lower()[1:]
318 return ext == extension
323 def clean_description(description):
324 'Remove unneeded newlines and spaces from a block of description text'
326 # this is split up by paragraph to make removing the newlines easier
327 for paragraph in re.split(r'\n\n', description):
328 paragraph = re.sub('\r', '', paragraph)
329 paragraph = re.sub('\n', ' ', paragraph)
330 paragraph = re.sub(' {2,}', ' ', paragraph)
331 paragraph = re.sub('^\s*(\w)', r'\1', paragraph)
332 returnstring += paragraph + '\n\n'
333 return returnstring.rstrip('\n')
336 def apknameinfo(filename):
338 filename = os.path.basename(filename)
339 if apk_regex is None:
340 apk_regex = re.compile(r"^(.+)_([0-9]+)\.apk$")
341 m = apk_regex.match(filename)
343 result = (m.group(1), m.group(2))
344 except AttributeError:
345 raise FDroidException("Invalid apk name: %s" % filename)
349 def getapkname(app, build):
350 return "%s_%s.apk" % (app['id'], build['vercode'])
353 def getsrcname(app, build):
354 return "%s_%s_src.tar.gz" % (app['id'], build['vercode'])
361 return app['Auto Name']
366 return '%s (%s)' % (app['Current Version'], app['Current Version Code'])
369 def getvcs(vcstype, remote, local):
371 return vcs_git(remote, local)
372 if vcstype == 'git-svn':
373 return vcs_gitsvn(remote, local)
375 return vcs_hg(remote, local)
377 return vcs_bzr(remote, local)
378 if vcstype == 'srclib':
379 if local != os.path.join('build', 'srclib', remote):
380 raise VCSException("Error: srclib paths are hard-coded!")
381 return getsrclib(remote, os.path.join('build', 'srclib'), raw=True)
383 raise VCSException("Deprecated vcs type 'svn' - please use 'git-svn' instead")
384 raise VCSException("Invalid vcs type " + vcstype)
387 def getsrclibvcs(name):
388 if name not in metadata.srclibs:
389 raise VCSException("Missing srclib " + name)
390 return metadata.srclibs[name]['Repo Type']
395 def __init__(self, remote, local):
397 # svn, git-svn and bzr may require auth
399 if self.repotype() in ('git-svn', 'bzr'):
401 self.username, remote = remote.split('@')
402 if ':' not in self.username:
403 raise VCSException("Password required with username")
404 self.username, self.password = self.username.split(':')
408 self.clone_failed = False
409 self.refreshed = False
415 # Take the local repository to a clean version of the given revision, which
416 # is specificed in the VCS's native format. Beforehand, the repository can
417 # be dirty, or even non-existent. If the repository does already exist
418 # locally, it will be updated from the origin, but only once in the
419 # lifetime of the vcs object.
420 # None is acceptable for 'rev' if you know you are cloning a clean copy of
421 # the repo - otherwise it must specify a valid revision.
422 def gotorevision(self, rev):
424 if self.clone_failed:
425 raise VCSException("Downloading the repository already failed once, not trying again.")
427 # The .fdroidvcs-id file for a repo tells us what VCS type
428 # and remote that directory was created from, allowing us to drop it
429 # automatically if either of those things changes.
430 fdpath = os.path.join(self.local, '..',
431 '.fdroidvcs-' + os.path.basename(self.local))
432 cdata = self.repotype() + ' ' + self.remote
435 if os.path.exists(self.local):
436 if os.path.exists(fdpath):
437 with open(fdpath, 'r') as f:
438 fsdata = f.read().strip()
443 logging.info("Repository details for %s changed - deleting" % (
447 logging.info("Repository details for %s missing - deleting" % (
450 shutil.rmtree(self.local)
455 self.gotorevisionx(rev)
456 except FDroidException, e:
459 # If necessary, write the .fdroidvcs file.
460 if writeback and not self.clone_failed:
461 with open(fdpath, 'w') as f:
467 # Derived classes need to implement this. It's called once basic checking
468 # has been performend.
469 def gotorevisionx(self, rev):
470 raise VCSException("This VCS type doesn't define gotorevisionx")
472 # Initialise and update submodules
473 def initsubmodules(self):
474 raise VCSException('Submodules not supported for this vcs type')
476 # Get a list of all known tags
478 raise VCSException('gettags not supported for this vcs type')
480 # Get a list of latest number tags
481 def latesttags(self, number):
482 raise VCSException('latesttags not supported for this vcs type')
484 # Get current commit reference (hash, revision, etc)
486 raise VCSException('getref not supported for this vcs type')
488 # Returns the srclib (name, path) used in setting up the current
499 # If the local directory exists, but is somehow not a git repository, git
500 # will traverse up the directory tree until it finds one that is (i.e.
501 # fdroidserver) and then we'll proceed to destroy it! This is called as
504 p = FDroidPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local, output=False)
505 result = p.output.rstrip()
506 if not result.endswith(self.local):
507 raise VCSException('Repository mismatch')
509 def gotorevisionx(self, rev):
510 if not os.path.exists(self.local):
512 p = FDroidPopen(['git', 'clone', self.remote, self.local])
513 if p.returncode != 0:
514 self.clone_failed = True
515 raise VCSException("Git clone failed", p.output)
519 # Discard any working tree changes
520 p = FDroidPopen(['git', 'reset', '--hard'], cwd=self.local, output=False)
521 if p.returncode != 0:
522 raise VCSException("Git reset failed", p.output)
523 # Remove untracked files now, in case they're tracked in the target
524 # revision (it happens!)
525 p = FDroidPopen(['git', 'clean', '-dffx'], cwd=self.local, output=False)
526 if p.returncode != 0:
527 raise VCSException("Git clean failed", p.output)
528 if not self.refreshed:
529 # Get latest commits and tags from remote
530 p = FDroidPopen(['git', 'fetch', 'origin'], cwd=self.local)
531 if p.returncode != 0:
532 raise VCSException("Git fetch failed", p.output)
533 p = FDroidPopen(['git', 'fetch', '--prune', '--tags', 'origin'], cwd=self.local, output=False)
534 if p.returncode != 0:
535 raise VCSException("Git fetch failed", p.output)
536 # Recreate origin/HEAD as git clone would do it, in case it disappeared
537 p = FDroidPopen(['git', 'remote', 'set-head', 'origin', '--auto'], cwd=self.local, output=False)
538 if p.returncode != 0:
539 lines = p.output.splitlines()
540 if 'Multiple remote HEAD branches' not in lines[0]:
541 raise VCSException("Git remote set-head failed", p.output)
542 branch = lines[1].split(' ')[-1]
543 p2 = FDroidPopen(['git', 'remote', 'set-head', 'origin', branch], cwd=self.local, output=False)
544 if p2.returncode != 0:
545 raise VCSException("Git remote set-head failed", p.output + '\n' + p2.output)
546 self.refreshed = True
547 # origin/HEAD is the HEAD of the remote, e.g. the "default branch" on
548 # a github repo. Most of the time this is the same as origin/master.
549 rev = rev or 'origin/HEAD'
550 p = FDroidPopen(['git', 'checkout', '-f', rev], cwd=self.local, output=False)
551 if p.returncode != 0:
552 raise VCSException("Git checkout of '%s' failed" % rev, p.output)
553 # Get rid of any uncontrolled files left behind
554 p = FDroidPopen(['git', 'clean', '-dffx'], cwd=self.local, output=False)
555 if p.returncode != 0:
556 raise VCSException("Git clean failed", p.output)
558 def initsubmodules(self):
560 submfile = os.path.join(self.local, '.gitmodules')
561 if not os.path.isfile(submfile):
562 raise VCSException("No git submodules available")
564 # fix submodules not accessible without an account and public key auth
565 with open(submfile, 'r') as f:
566 lines = f.readlines()
567 with open(submfile, 'w') as f:
569 if 'git@github.com' in line:
570 line = line.replace('git@github.com:', 'https://github.com/')
574 ['git', 'reset', '--hard'],
575 ['git', 'clean', '-dffx'],
577 p = FDroidPopen(['git', 'submodule', 'foreach', '--recursive'] + cmd, cwd=self.local, output=False)
578 if p.returncode != 0:
579 raise VCSException("Git submodule reset failed", p.output)
580 p = FDroidPopen(['git', 'submodule', 'sync'], cwd=self.local, output=False)
581 if p.returncode != 0:
582 raise VCSException("Git submodule sync failed", p.output)
583 p = FDroidPopen(['git', 'submodule', 'update', '--init', '--force', '--recursive'], cwd=self.local)
584 if p.returncode != 0:
585 raise VCSException("Git submodule update failed", p.output)
589 p = FDroidPopen(['git', 'tag'], cwd=self.local, output=False)
590 return p.output.splitlines()
592 def latesttags(self, alltags, number):
594 p = FDroidPopen(['echo "' + '\n'.join(alltags) + '" | '
596 'xargs -I@ git log --format=format:"%at @%n" -1 @ | '
597 + 'sort -n | awk \'{print $2}\''],
598 cwd=self.local, shell=True, output=False)
599 return p.output.splitlines()[-number:]
602 class vcs_gitsvn(vcs):
607 # Damn git-svn tries to use a graphical password prompt, so we have to
608 # trick it into taking the password from stdin
610 if self.username is None:
612 return ('echo "%s" | DISPLAY="" ' % self.password, ' --username "%s"' % self.username)
614 # If the local directory exists, but is somehow not a git repository, git
615 # will traverse up the directory tree until it finds one that is (i.e.
616 # fdroidserver) and then we'll proceed to destory it! This is called as
619 p = FDroidPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local, output=False)
620 result = p.output.rstrip()
621 if not result.endswith(self.local):
622 raise VCSException('Repository mismatch')
624 def gotorevisionx(self, rev):
625 if not os.path.exists(self.local):
627 gitsvn_cmd = '%sgit svn clone%s' % self.userargs()
628 if ';' in self.remote:
629 remote_split = self.remote.split(';')
630 for i in remote_split[1:]:
631 if i.startswith('trunk='):
632 gitsvn_cmd += ' -T %s' % i[6:]
633 elif i.startswith('tags='):
634 gitsvn_cmd += ' -t %s' % i[5:]
635 elif i.startswith('branches='):
636 gitsvn_cmd += ' -b %s' % i[9:]
637 p = FDroidPopen([gitsvn_cmd + " %s %s" % (remote_split[0], self.local)], shell=True, output=False)
638 if p.returncode != 0:
639 self.clone_failed = True
640 raise VCSException("Git svn clone failed", p.output)
642 p = FDroidPopen([gitsvn_cmd + " %s %s" % (self.remote, self.local)], shell=True, output=False)
643 if p.returncode != 0:
644 self.clone_failed = True
645 raise VCSException("Git svn clone failed", p.output)
649 # Discard any working tree changes
650 p = FDroidPopen(['git', 'reset', '--hard'], cwd=self.local, output=False)
651 if p.returncode != 0:
652 raise VCSException("Git reset failed", p.output)
653 # Remove untracked files now, in case they're tracked in the target
654 # revision (it happens!)
655 p = FDroidPopen(['git', 'clean', '-dffx'], cwd=self.local, output=False)
656 if p.returncode != 0:
657 raise VCSException("Git clean failed", p.output)
658 if not self.refreshed:
659 # Get new commits, branches and tags from repo
660 p = FDroidPopen(['%sgit svn fetch %s' % self.userargs()], cwd=self.local, shell=True, output=False)
661 if p.returncode != 0:
662 raise VCSException("Git svn fetch failed")
663 p = FDroidPopen(['%sgit svn rebase %s' % self.userargs()], cwd=self.local, shell=True, output=False)
664 if p.returncode != 0:
665 raise VCSException("Git svn rebase failed", p.output)
666 self.refreshed = True
668 rev = rev or 'master'
670 nospaces_rev = rev.replace(' ', '%20')
671 # Try finding a svn tag
672 for treeish in ['origin/', '']:
673 p = FDroidPopen(['git', 'checkout', treeish + 'tags/' + nospaces_rev], cwd=self.local, output=False)
674 if p.returncode == 0:
676 if p.returncode != 0:
677 # No tag found, normal svn rev translation
678 # Translate svn rev into git format
679 rev_split = rev.split('/')
682 for treeish in ['origin/', '']:
683 if len(rev_split) > 1:
684 treeish += rev_split[0]
685 svn_rev = rev_split[1]
688 # if no branch is specified, then assume trunk (i.e. 'master' branch):
692 svn_rev = svn_rev if svn_rev[0] == 'r' else 'r' + svn_rev
694 p = FDroidPopen(['git', 'svn', 'find-rev', '--before', svn_rev, treeish], cwd=self.local, output=False)
695 git_rev = p.output.rstrip()
697 if p.returncode == 0 and git_rev:
700 if p.returncode != 0 or not git_rev:
701 # Try a plain git checkout as a last resort
702 p = FDroidPopen(['git', 'checkout', rev], cwd=self.local, output=False)
703 if p.returncode != 0:
704 raise VCSException("No git treeish found and direct git checkout of '%s' failed" % rev, p.output)
706 # Check out the git rev equivalent to the svn rev
707 p = FDroidPopen(['git', 'checkout', git_rev], cwd=self.local, output=False)
708 if p.returncode != 0:
709 raise VCSException("Git checkout of '%s' failed" % rev, p.output)
711 # Get rid of any uncontrolled files left behind
712 p = FDroidPopen(['git', 'clean', '-dffx'], cwd=self.local, output=False)
713 if p.returncode != 0:
714 raise VCSException("Git clean failed", p.output)
718 for treeish in ['origin/', '']:
719 d = os.path.join(self.local, '.git', 'svn', 'refs', 'remotes', treeish, 'tags')
725 p = FDroidPopen(['git', 'svn', 'find-rev', 'HEAD'], cwd=self.local, output=False)
726 if p.returncode != 0:
728 return p.output.strip()
736 def gotorevisionx(self, rev):
737 if not os.path.exists(self.local):
738 p = FDroidPopen(['hg', 'clone', self.remote, self.local], output=False)
739 if p.returncode != 0:
740 self.clone_failed = True
741 raise VCSException("Hg clone failed", p.output)
743 p = FDroidPopen(['hg status -uS | xargs rm -rf'], cwd=self.local, shell=True, output=False)
744 if p.returncode != 0:
745 raise VCSException("Hg clean failed", p.output)
746 if not self.refreshed:
747 p = FDroidPopen(['hg', 'pull'], cwd=self.local, output=False)
748 if p.returncode != 0:
749 raise VCSException("Hg pull failed", p.output)
750 self.refreshed = True
752 rev = rev or 'default'
755 p = FDroidPopen(['hg', 'update', '-C', rev], cwd=self.local, output=False)
756 if p.returncode != 0:
757 raise VCSException("Hg checkout of '%s' failed" % rev, p.output)
758 p = FDroidPopen(['hg', 'purge', '--all'], cwd=self.local, output=False)
759 # Also delete untracked files, we have to enable purge extension for that:
760 if "'purge' is provided by the following extension" in p.output:
761 with open(os.path.join(self.local, '.hg', 'hgrc'), "a") as myfile:
762 myfile.write("\n[extensions]\nhgext.purge=\n")
763 p = FDroidPopen(['hg', 'purge', '--all'], cwd=self.local, output=False)
764 if p.returncode != 0:
765 raise VCSException("HG purge failed", p.output)
766 elif p.returncode != 0:
767 raise VCSException("HG purge failed", p.output)
770 p = FDroidPopen(['hg', 'tags', '-q'], cwd=self.local, output=False)
771 return p.output.splitlines()[1:]
779 def gotorevisionx(self, rev):
780 if not os.path.exists(self.local):
781 p = FDroidPopen(['bzr', 'branch', self.remote, self.local], output=False)
782 if p.returncode != 0:
783 self.clone_failed = True
784 raise VCSException("Bzr branch failed", p.output)
786 p = FDroidPopen(['bzr', 'clean-tree', '--force', '--unknown', '--ignored'], cwd=self.local, output=False)
787 if p.returncode != 0:
788 raise VCSException("Bzr revert failed", p.output)
789 if not self.refreshed:
790 p = FDroidPopen(['bzr', 'pull'], cwd=self.local, output=False)
791 if p.returncode != 0:
792 raise VCSException("Bzr update failed", p.output)
793 self.refreshed = True
795 revargs = list(['-r', rev] if rev else [])
796 p = FDroidPopen(['bzr', 'revert'] + revargs, cwd=self.local, output=False)
797 if p.returncode != 0:
798 raise VCSException("Bzr revert of '%s' failed" % rev, p.output)
801 p = FDroidPopen(['bzr', 'tags'], cwd=self.local, output=False)
802 return [tag.split(' ')[0].strip() for tag in
803 p.output.splitlines()]
806 def retrieve_string(app_dir, string, xmlfiles=None):
809 os.path.join(app_dir, 'res'),
810 os.path.join(app_dir, 'src', 'main'),
815 for res_dir in res_dirs:
816 for r, d, f in os.walk(res_dir):
817 if os.path.basename(r) == 'values':
818 xmlfiles += [os.path.join(r, x) for x in f if x.endswith('.xml')]
821 if string.startswith('@string/'):
822 string_search = re.compile(r'.*name="' + string[8:] + '".*?>"?([^<]+?)"?<.*').search
823 elif string.startswith('&') and string.endswith(';'):
824 string_search = re.compile(r'.*<!ENTITY.*' + string[1:-1] + '.*?"([^"]+?)".*>').search
826 if string_search is not None:
827 for xmlfile in xmlfiles:
828 for line in file(xmlfile):
829 matches = string_search(line)
831 return retrieve_string(app_dir, matches.group(1), xmlfiles)
834 return string.replace("\\'", "'")
837 # Return list of existing files that will be used to find the highest vercode
838 def manifest_paths(app_dir, flavours):
840 possible_manifests = \
841 [os.path.join(app_dir, 'AndroidManifest.xml'),
842 os.path.join(app_dir, 'src', 'main', 'AndroidManifest.xml'),
843 os.path.join(app_dir, 'src', 'AndroidManifest.xml'),
844 os.path.join(app_dir, 'build.gradle')]
846 for flavour in flavours:
849 possible_manifests.append(
850 os.path.join(app_dir, 'src', flavour, 'AndroidManifest.xml'))
852 return [path for path in possible_manifests if os.path.isfile(path)]
855 # Retrieve the package name. Returns the name, or None if not found.
856 def fetch_real_name(app_dir, flavours):
857 app_search = re.compile(r'.*<application.*').search
858 name_search = re.compile(r'.*android:label="([^"]+)".*').search
860 for f in manifest_paths(app_dir, flavours):
861 if not has_extension(f, 'xml'):
863 logging.debug("fetch_real_name: Checking manifest at " + f)
869 matches = name_search(line)
871 stringname = matches.group(1)
872 logging.debug("fetch_real_name: using string " + stringname)
873 result = retrieve_string(app_dir, stringname)
875 result = result.strip()
880 # Retrieve the version name
881 def version_name(original, app_dir, flavours):
882 for f in manifest_paths(app_dir, flavours):
883 if not has_extension(f, 'xml'):
885 string = retrieve_string(app_dir, original)
891 def get_library_references(root_dir):
893 proppath = os.path.join(root_dir, 'project.properties')
894 if not os.path.isfile(proppath):
896 with open(proppath) as f:
897 for line in f.readlines():
898 if not line.startswith('android.library.reference.'):
900 path = line.split('=')[1].strip()
901 relpath = os.path.join(root_dir, path)
902 if not os.path.isdir(relpath):
904 logging.debug("Found subproject at %s" % path)
905 libraries.append(path)
909 def ant_subprojects(root_dir):
910 subprojects = get_library_references(root_dir)
911 for subpath in subprojects:
912 subrelpath = os.path.join(root_dir, subpath)
913 for p in get_library_references(subrelpath):
914 relp = os.path.normpath(os.path.join(subpath, p))
915 if relp not in subprojects:
916 subprojects.insert(0, relp)
920 def remove_debuggable_flags(root_dir):
921 # Remove forced debuggable flags
922 logging.debug("Removing debuggable flags from %s" % root_dir)
923 for root, dirs, files in os.walk(root_dir):
924 if 'AndroidManifest.xml' in files:
925 path = os.path.join(root, 'AndroidManifest.xml')
926 p = FDroidPopen(['sed', '-i', 's/android:debuggable="[^"]*"//g', path], output=False)
927 if p.returncode != 0:
928 raise BuildException("Failed to remove debuggable flags of %s" % path)
931 # Extract some information from the AndroidManifest.xml at the given path.
932 # Returns (version, vercode, package), any or all of which might be None.
933 # All values returned are strings.
934 def parse_androidmanifests(paths, ignoreversions=None):
937 return (None, None, None)
939 vcsearch = re.compile(r'.*:versionCode="([0-9]+?)".*').search
940 vnsearch = re.compile(r'.*:versionName="([^"]+?)".*').search
941 psearch = re.compile(r'.*package="([^"]+)".*').search
943 vcsearch_g = re.compile(r'.*versionCode *=* *["\']*([0-9]+)["\']*').search
944 vnsearch_g = re.compile(r'.*versionName *=* *(["\'])((?:(?=(\\?))\3.)*?)\1.*').search
945 psearch_g = re.compile(r'.*packageName *=* *["\']([^"]+)["\'].*').search
947 ignoresearch = re.compile(ignoreversions).search if ignoreversions else None
955 gradle = has_extension(path, 'gradle')
958 # Remember package name, may be defined separately from version+vercode
959 package = max_package
961 for line in file(path):
964 matches = psearch_g(line)
966 matches = psearch(line)
968 package = matches.group(1)
971 matches = vnsearch_g(line)
973 matches = vnsearch(line)
975 version = matches.group(2 if gradle else 1)
978 matches = vcsearch_g(line)
980 matches = vcsearch(line)
982 vercode = matches.group(1)
984 # Always grab the package name and version name in case they are not
985 # together with the highest version code
986 if max_package is None and package is not None:
987 max_package = package
988 if max_version is None and version is not None:
989 max_version = version
991 if max_vercode is None or (vercode is not None and vercode > max_vercode):
992 if not ignoresearch or not ignoresearch(version):
993 if version is not None:
994 max_version = version
995 if vercode is not None:
996 max_vercode = vercode
997 if package is not None:
998 max_package = package
1000 max_version = "Ignore"
1002 if max_version is None:
1003 max_version = "Unknown"
1005 return (max_version, max_vercode, max_package)
1008 class FDroidException(Exception):
1010 def __init__(self, value, detail=None):
1012 self.detail = detail
1014 def get_wikitext(self):
1015 ret = repr(self.value) + "\n"
1019 txt = self.detail[-8192:] if len(self.detail) > 8192 else self.detail
1027 ret += "\n==== detail begin ====\n%s\n==== detail end ====" % self.detail.strip()
1031 class VCSException(FDroidException):
1035 class BuildException(FDroidException):
1039 # Get the specified source library.
1040 # Returns the path to it. Normally this is the path to be used when referencing
1041 # it, which may be a subdirectory of the actual project. If you want the base
1042 # directory of the project, pass 'basepath=True'.
1043 def getsrclib(spec, srclib_dir, srclibpaths=[], subdir=None,
1044 basepath=False, raw=False, prepare=True, preponly=False):
1052 name, ref = spec.split('@')
1054 number, name = name.split(':', 1)
1056 name, subdir = name.split('/', 1)
1058 if name not in metadata.srclibs:
1059 raise VCSException('srclib ' + name + ' not found.')
1061 srclib = metadata.srclibs[name]
1063 sdir = os.path.join(srclib_dir, name)
1066 vcs = getvcs(srclib["Repo Type"], srclib["Repo"], sdir)
1067 vcs.srclib = (name, number, sdir)
1069 vcs.gotorevision(ref)
1076 libdir = os.path.join(sdir, subdir)
1077 elif srclib["Subdir"]:
1078 for subdir in srclib["Subdir"]:
1079 libdir_candidate = os.path.join(sdir, subdir)
1080 if os.path.exists(libdir_candidate):
1081 libdir = libdir_candidate
1087 if srclib["Srclibs"]:
1089 for lib in srclib["Srclibs"].replace(';', ',').split(','):
1091 for t in srclibpaths:
1096 raise VCSException('Missing recursive srclib %s for %s' % (
1098 place_srclib(libdir, n, s_tuple[2])
1101 remove_signing_keys(sdir)
1102 remove_debuggable_flags(sdir)
1106 if srclib["Prepare"]:
1107 cmd = replace_config_vars(srclib["Prepare"])
1109 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=libdir)
1110 if p.returncode != 0:
1111 raise BuildException("Error running prepare command for srclib %s"
1117 return (name, number, libdir)
1120 # Prepare the source code for a particular build
1121 # 'vcs' - the appropriate vcs object for the application
1122 # 'app' - the application details from the metadata
1123 # 'build' - the build details from the metadata
1124 # 'build_dir' - the path to the build directory, usually
1126 # 'srclib_dir' - the path to the source libraries directory, usually
1128 # 'extlib_dir' - the path to the external libraries directory, usually
1130 # Returns the (root, srclibpaths) where:
1131 # 'root' is the root directory, which may be the same as 'build_dir' or may
1132 # be a subdirectory of it.
1133 # 'srclibpaths' is information on the srclibs being used
1134 def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=False):
1136 # Optionally, the actual app source can be in a subdirectory
1138 root_dir = os.path.join(build_dir, build['subdir'])
1140 root_dir = build_dir
1142 # Get a working copy of the right revision
1143 logging.info("Getting source for revision " + build['commit'])
1144 vcs.gotorevision(build['commit'])
1146 # Initialise submodules if requred
1147 if build['submodules']:
1148 logging.info("Initialising submodules")
1149 vcs.initsubmodules()
1151 # Check that a subdir (if we're using one) exists. This has to happen
1152 # after the checkout, since it might not exist elsewhere
1153 if not os.path.exists(root_dir):
1154 raise BuildException('Missing subdir ' + root_dir)
1156 # Run an init command if one is required
1158 cmd = replace_config_vars(build['init'])
1159 logging.info("Running 'init' commands in %s" % root_dir)
1161 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
1162 if p.returncode != 0:
1163 raise BuildException("Error running init command for %s:%s" %
1164 (app['id'], build['version']), p.output)
1166 # Apply patches if any
1168 logging.info("Applying patches")
1169 for patch in build['patch']:
1170 patch = patch.strip()
1171 logging.info("Applying " + patch)
1172 patch_path = os.path.join('metadata', app['id'], patch)
1173 p = FDroidPopen(['patch', '-p1', '-i', os.path.abspath(patch_path)], cwd=build_dir)
1174 if p.returncode != 0:
1175 raise BuildException("Failed to apply patch %s" % patch_path)
1177 # Get required source libraries
1179 if build['srclibs']:
1180 logging.info("Collecting source libraries")
1181 for lib in build['srclibs']:
1182 srclibpaths.append(getsrclib(lib, srclib_dir, srclibpaths,
1185 for name, number, libpath in srclibpaths:
1186 place_srclib(root_dir, int(number) if number else None, libpath)
1188 basesrclib = vcs.getsrclib()
1189 # If one was used for the main source, add that too.
1191 srclibpaths.append(basesrclib)
1193 # Update the local.properties file
1194 localprops = [os.path.join(build_dir, 'local.properties')]
1196 localprops += [os.path.join(root_dir, 'local.properties')]
1197 for path in localprops:
1199 if os.path.isfile(path):
1200 logging.info("Updating local.properties file at %s" % path)
1206 logging.info("Creating local.properties file at %s" % path)
1207 # Fix old-fashioned 'sdk-location' by copying
1208 # from sdk.dir, if necessary
1209 if build['oldsdkloc']:
1210 sdkloc = re.match(r".*^sdk.dir=(\S+)$.*", props,
1211 re.S | re.M).group(1)
1212 props += "sdk-location=%s\n" % sdkloc
1214 props += "sdk.dir=%s\n" % config['sdk_path']
1215 props += "sdk-location=%s\n" % config['sdk_path']
1216 if config['ndk_path']:
1218 props += "ndk.dir=%s\n" % config['ndk_path']
1219 props += "ndk-location=%s\n" % config['ndk_path']
1220 # Add java.encoding if necessary
1221 if build['encoding']:
1222 props += "java.encoding=%s\n" % build['encoding']
1228 if build['type'] == 'gradle':
1229 flavours = build['gradle']
1231 version_regex = re.compile(r".*'com\.android\.tools\.build:gradle:([^\.]+\.[^\.]+).*'.*")
1232 gradlepluginver = None
1234 gradle_files = [os.path.join(root_dir, 'build.gradle')]
1236 # Parent dir build.gradle
1237 parent_dir = os.path.normpath(os.path.join(root_dir, '..'))
1238 if parent_dir.startswith(build_dir):
1239 gradle_files.append(os.path.join(parent_dir, 'build.gradle'))
1241 for path in gradle_files:
1244 if not os.path.isfile(path):
1246 with open(path) as f:
1248 match = version_regex.match(line)
1250 gradlepluginver = match.group(1)
1254 build['gradlepluginver'] = LooseVersion(gradlepluginver)
1256 logging.warn("Could not fetch the gradle plugin version, defaulting to 0.11")
1257 build['gradlepluginver'] = LooseVersion('0.11')
1260 n = build["target"].split('-')[1]
1261 FDroidPopen(['sed', '-i',
1262 's@compileSdkVersion *[0-9]*@compileSdkVersion ' + n + '@g',
1263 'build.gradle'], cwd=root_dir, output=False)
1265 # Remove forced debuggable flags
1266 remove_debuggable_flags(root_dir)
1268 # Insert version code and number into the manifest if necessary
1269 if build['forceversion']:
1270 logging.info("Changing the version name")
1271 for path in manifest_paths(root_dir, flavours):
1272 if not os.path.isfile(path):
1274 if has_extension(path, 'xml'):
1275 p = FDroidPopen(['sed', '-i',
1276 's/android:versionName="[^"]*"/android:versionName="' + build['version'] + '"/g',
1277 path], output=False)
1278 if p.returncode != 0:
1279 raise BuildException("Failed to amend manifest")
1280 elif has_extension(path, 'gradle'):
1281 p = FDroidPopen(['sed', '-i',
1282 's/versionName *=* *"[^"]*"/versionName = "' + build['version'] + '"/g',
1283 path], output=False)
1284 if p.returncode != 0:
1285 raise BuildException("Failed to amend build.gradle")
1286 if build['forcevercode']:
1287 logging.info("Changing the version code")
1288 for path in manifest_paths(root_dir, flavours):
1289 if not os.path.isfile(path):
1291 if has_extension(path, 'xml'):
1292 p = FDroidPopen(['sed', '-i',
1293 's/android:versionCode="[^"]*"/android:versionCode="' + build['vercode'] + '"/g',
1294 path], output=False)
1295 if p.returncode != 0:
1296 raise BuildException("Failed to amend manifest")
1297 elif has_extension(path, 'gradle'):
1298 p = FDroidPopen(['sed', '-i',
1299 's/versionCode *=* *[0-9]*/versionCode = ' + build['vercode'] + '/g',
1300 path], output=False)
1301 if p.returncode != 0:
1302 raise BuildException("Failed to amend build.gradle")
1304 # Delete unwanted files
1306 logging.info("Removing specified files")
1307 for part in getpaths(build_dir, build, 'rm'):
1308 dest = os.path.join(build_dir, part)
1309 logging.info("Removing {0}".format(part))
1310 if os.path.lexists(dest):
1311 if os.path.islink(dest):
1312 FDroidPopen(['unlink ' + dest], shell=True, output=False)
1314 FDroidPopen(['rm -rf ' + dest], shell=True, output=False)
1316 logging.info("...but it didn't exist")
1318 remove_signing_keys(build_dir)
1320 # Add required external libraries
1321 if build['extlibs']:
1322 logging.info("Collecting prebuilt libraries")
1323 libsdir = os.path.join(root_dir, 'libs')
1324 if not os.path.exists(libsdir):
1326 for lib in build['extlibs']:
1328 logging.info("...installing extlib {0}".format(lib))
1329 libf = os.path.basename(lib)
1330 libsrc = os.path.join(extlib_dir, lib)
1331 if not os.path.exists(libsrc):
1332 raise BuildException("Missing extlib file {0}".format(libsrc))
1333 shutil.copyfile(libsrc, os.path.join(libsdir, libf))
1335 # Run a pre-build command if one is required
1336 if build['prebuild']:
1337 logging.info("Running 'prebuild' commands in %s" % root_dir)
1339 cmd = replace_config_vars(build['prebuild'])
1341 # Substitute source library paths into prebuild commands
1342 for name, number, libpath in srclibpaths:
1343 libpath = os.path.relpath(libpath, root_dir)
1344 cmd = cmd.replace('$$' + name + '$$', libpath)
1346 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
1347 if p.returncode != 0:
1348 raise BuildException("Error running prebuild command for %s:%s" %
1349 (app['id'], build['version']), p.output)
1351 # Generate (or update) the ant build file, build.xml...
1352 if build['update'] and build['update'] != ['no'] and build['type'] == 'ant':
1353 parms = ['android', 'update', 'lib-project']
1354 lparms = ['android', 'update', 'project']
1357 parms += ['-t', build['target']]
1358 lparms += ['-t', build['target']]
1359 if build['update'] == ['auto']:
1360 update_dirs = ant_subprojects(root_dir) + ['.']
1362 update_dirs = build['update']
1364 for d in update_dirs:
1365 subdir = os.path.join(root_dir, d)
1367 logging.debug("Updating main project")
1368 cmd = parms + ['-p', d]
1370 logging.debug("Updating subproject %s" % d)
1371 cmd = lparms + ['-p', d]
1372 p = SdkToolsPopen(cmd, cwd=root_dir)
1373 # Check to see whether an error was returned without a proper exit
1374 # code (this is the case for the 'no target set or target invalid'
1376 if p.returncode != 0 or p.output.startswith("Error: "):
1377 raise BuildException("Failed to update project at %s" % d, p.output)
1378 # Clean update dirs via ant
1380 logging.info("Cleaning subproject %s" % d)
1381 p = FDroidPopen(['ant', 'clean'], cwd=subdir)
1383 return (root_dir, srclibpaths)
1386 # Split and extend via globbing the paths from a field
1387 def getpaths(build_dir, build, field):
1389 for p in build[field]:
1391 full_path = os.path.join(build_dir, p)
1392 full_path = os.path.normpath(full_path)
1393 paths += [r[len(build_dir) + 1:] for r in glob.glob(full_path)]
1397 # Scan the source code in the given directory (and all subdirectories)
1398 # and return the number of fatal problems encountered
1399 def scan_source(build_dir, root_dir, thisbuild):
1403 # Common known non-free blobs (always lower case):
1405 re.compile(r'flurryagent', re.IGNORECASE),
1406 re.compile(r'paypal.*mpl', re.IGNORECASE),
1407 re.compile(r'google.*analytics', re.IGNORECASE),
1408 re.compile(r'admob.*sdk.*android', re.IGNORECASE),
1409 re.compile(r'google.*ad.*view', re.IGNORECASE),
1410 re.compile(r'google.*admob', re.IGNORECASE),
1411 re.compile(r'google.*play.*services', re.IGNORECASE),
1412 re.compile(r'crittercism', re.IGNORECASE),
1413 re.compile(r'heyzap', re.IGNORECASE),
1414 re.compile(r'jpct.*ae', re.IGNORECASE),
1415 re.compile(r'youtube.*android.*player.*api', re.IGNORECASE),
1416 re.compile(r'bugsense', re.IGNORECASE),
1417 re.compile(r'crashlytics', re.IGNORECASE),
1418 re.compile(r'ouya.*sdk', re.IGNORECASE),
1419 re.compile(r'libspen23', re.IGNORECASE),
1422 scanignore = getpaths(build_dir, thisbuild, 'scanignore')
1423 scandelete = getpaths(build_dir, thisbuild, 'scandelete')
1426 ms = magic.open(magic.MIME_TYPE)
1428 except AttributeError:
1432 for i in scanignore:
1433 if fd.startswith(i):
1438 for i in scandelete:
1439 if fd.startswith(i):
1443 def removeproblem(what, fd, fp):
1444 logging.info('Removing %s at %s' % (what, fd))
1447 def warnproblem(what, fd):
1448 logging.warn('Found %s at %s' % (what, fd))
1450 def handleproblem(what, fd, fp):
1452 logging.info('Ignoring %s at %s' % (what, fd))
1454 removeproblem(what, fd, fp)
1456 logging.error('Found %s at %s' % (what, fd))
1460 # Iterate through all files in the source code
1461 for r, d, f in os.walk(build_dir, topdown=True):
1463 # It's topdown, so checking the basename is enough
1464 for ignoredir in ('.hg', '.git', '.svn', '.bzr'):
1470 # Path (relative) to the file
1471 fp = os.path.join(r, curfile)
1472 fd = fp[len(build_dir) + 1:]
1475 mime = magic.from_file(fp, mime=True) if ms is None else ms.file(fp)
1476 except UnicodeError:
1477 warnproblem('malformed magic number', fd)
1479 if mime == 'application/x-sharedlib':
1480 count += handleproblem('shared library', fd, fp)
1482 elif mime == 'application/x-archive':
1483 count += handleproblem('static library', fd, fp)
1485 elif mime == 'application/x-executable':
1486 count += handleproblem('binary executable', fd, fp)
1488 elif mime == 'application/x-java-applet':
1489 count += handleproblem('Java compiled class', fd, fp)
1494 'application/java-archive',
1495 'application/octet-stream',
1499 if has_extension(fp, 'apk'):
1500 removeproblem('APK file', fd, fp)
1502 elif has_extension(fp, 'jar'):
1504 if any(suspect.match(curfile) for suspect in usual_suspects):
1505 count += handleproblem('usual supect', fd, fp)
1507 warnproblem('JAR file', fd)
1509 elif has_extension(fp, 'zip'):
1510 warnproblem('ZIP file', fd)
1513 warnproblem('unknown compressed or binary file', fd)
1515 elif has_extension(fp, 'java'):
1516 for line in file(fp):
1517 if 'DexClassLoader' in line:
1518 count += handleproblem('DexClassLoader', fd, fp)
1523 # Presence of a jni directory without buildjni=yes might
1524 # indicate a problem (if it's not a problem, explicitly use
1525 # buildjni=no to bypass this check)
1526 if (os.path.exists(os.path.join(root_dir, 'jni')) and
1527 not thisbuild['buildjni']):
1528 logging.error('Found jni directory, but buildjni is not enabled. Set it to \'no\' to ignore.')
1537 self.path = os.path.join('stats', 'known_apks.txt')
1539 if os.path.exists(self.path):
1540 for line in file(self.path):
1541 t = line.rstrip().split(' ')
1543 self.apks[t[0]] = (t[1], None)
1545 self.apks[t[0]] = (t[1], time.strptime(t[2], '%Y-%m-%d'))
1546 self.changed = False
1548 def writeifchanged(self):
1550 if not os.path.exists('stats'):
1552 f = open(self.path, 'w')
1554 for apk, app in self.apks.iteritems():
1556 line = apk + ' ' + appid
1558 line += ' ' + time.strftime('%Y-%m-%d', added)
1560 for line in sorted(lst):
1561 f.write(line + '\n')
1564 # Record an apk (if it's new, otherwise does nothing)
1565 # Returns the date it was added.
1566 def recordapk(self, apk, app):
1567 if apk not in self.apks:
1568 self.apks[apk] = (app, time.gmtime(time.time()))
1570 _, added = self.apks[apk]
1573 # Look up information - given the 'apkname', returns (app id, date added/None).
1574 # Or returns None for an unknown apk.
1575 def getapp(self, apkname):
1576 if apkname in self.apks:
1577 return self.apks[apkname]
1580 # Get the most recent 'num' apps added to the repo, as a list of package ids
1581 # with the most recent first.
1582 def getlatest(self, num):
1584 for apk, app in self.apks.iteritems():
1588 if apps[appid] > added:
1592 sortedapps = sorted(apps.iteritems(), key=operator.itemgetter(1))[-num:]
1593 lst = [app for app, _ in sortedapps]
1598 def isApkDebuggable(apkfile, config):
1599 """Returns True if the given apk file is debuggable
1601 :param apkfile: full path to the apk to check"""
1603 p = SdkToolsPopen(['aapt', 'dump', 'xmltree', apkfile, 'AndroidManifest.xml'],
1605 if p.returncode != 0:
1606 logging.critical("Failed to get apk manifest information")
1608 for line in p.output.splitlines():
1609 if 'android:debuggable' in line and not line.endswith('0x0'):
1614 class AsynchronousFileReader(threading.Thread):
1617 Helper class to implement asynchronous reading of a file
1618 in a separate thread. Pushes read lines on a queue to
1619 be consumed in another thread.
1622 def __init__(self, fd, queue):
1623 assert isinstance(queue, Queue.Queue)
1624 assert callable(fd.readline)
1625 threading.Thread.__init__(self)
1630 '''The body of the tread: read lines and put them on the queue.'''
1631 for line in iter(self._fd.readline, ''):
1632 self._queue.put(line)
1635 '''Check whether there is no more content to expect.'''
1636 return not self.is_alive() and self._queue.empty()
1644 def SdkToolsPopen(commands, cwd=None, shell=False, output=True):
1646 if cmd not in config:
1647 config[cmd] = find_sdk_tools_cmd(commands[0])
1648 return FDroidPopen([config[cmd]] + commands[1:],
1649 cwd=cwd, shell=shell, output=output)
1652 def FDroidPopen(commands, cwd=None, shell=False, output=True):
1654 Run a command and capture the possibly huge output.
1656 :param commands: command and argument list like in subprocess.Popen
1657 :param cwd: optionally specifies a working directory
1658 :returns: A PopenResult.
1664 cwd = os.path.normpath(cwd)
1665 logging.debug("Directory: %s" % cwd)
1666 logging.debug("> %s" % ' '.join(commands))
1668 result = PopenResult()
1671 p = subprocess.Popen(commands, cwd=cwd, shell=shell, env=env,
1672 stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
1674 raise BuildException("OSError while trying to execute " + ' '.join(commands) + ': ' + str(e))
1676 stdout_queue = Queue.Queue()
1677 stdout_reader = AsynchronousFileReader(p.stdout, stdout_queue)
1678 stdout_reader.start()
1680 # Check the queue for output (until there is no more to get)
1681 while not stdout_reader.eof():
1682 while not stdout_queue.empty():
1683 line = stdout_queue.get()
1684 if output and options.verbose:
1685 # Output directly to console
1686 sys.stderr.write(line)
1688 result.output += line
1692 result.returncode = p.wait()
1696 def remove_signing_keys(build_dir):
1697 comment = re.compile(r'[ ]*//')
1698 signing_configs = re.compile(r'^[\t ]*signingConfigs[ \t]*{[ \t]*$')
1700 re.compile(r'^[\t ]*signingConfig [^ ]*$'),
1701 re.compile(r'.*android\.signingConfigs\.[^{]*$'),
1702 re.compile(r'.*variant\.outputFile = .*'),
1703 re.compile(r'.*output\.outputFile = .*'),
1704 re.compile(r'.*\.readLine\(.*'),
1706 for root, dirs, files in os.walk(build_dir):
1707 if 'build.gradle' in files:
1708 path = os.path.join(root, 'build.gradle')
1710 with open(path, "r") as o:
1711 lines = o.readlines()
1716 with open(path, "w") as o:
1718 if comment.match(line):
1722 opened += line.count('{')
1723 opened -= line.count('}')
1726 if signing_configs.match(line):
1731 if any(s.match(line) for s in line_matches):
1739 logging.info("Cleaned build.gradle of keysigning configs at %s" % path)
1742 'project.properties',
1744 'default.properties',
1747 if propfile in files:
1748 path = os.path.join(root, propfile)
1750 with open(path, "r") as o:
1751 lines = o.readlines()
1755 with open(path, "w") as o:
1757 if any(line.startswith(s) for s in ('key.store', 'key.alias')):
1764 logging.info("Cleaned %s of keysigning configs at %s" % (propfile, path))
1767 def replace_config_vars(cmd):
1768 cmd = cmd.replace('$$SDK$$', config['sdk_path'])
1769 cmd = cmd.replace('$$NDK$$', config['ndk_path'])
1770 cmd = cmd.replace('$$MVN3$$', config['mvn3'])
1774 def place_srclib(root_dir, number, libpath):
1777 relpath = os.path.relpath(libpath, root_dir)
1778 proppath = os.path.join(root_dir, 'project.properties')
1781 if os.path.isfile(proppath):
1782 with open(proppath, "r") as o:
1783 lines = o.readlines()
1785 with open(proppath, "w") as o:
1788 if line.startswith('android.library.reference.%d=' % number):
1789 o.write('android.library.reference.%d=%s\n' % (number, relpath))
1794 o.write('android.library.reference.%d=%s\n' % (number, relpath))
1797 def compare_apks(apk1, apk2, tmp_dir):
1800 Returns None if the apk content is the same (apart from the signing key),
1801 otherwise a string describing what's different, or what went wrong when
1802 trying to do the comparison.
1805 thisdir = os.path.join(tmp_dir, 'this_apk')
1806 thatdir = os.path.join(tmp_dir, 'that_apk')
1807 for d in [thisdir, thatdir]:
1808 if os.path.exists(d):
1812 if subprocess.call(['jar', 'xf',
1813 os.path.abspath(apk1)],
1815 return("Failed to unpack " + apk1)
1816 if subprocess.call(['jar', 'xf',
1817 os.path.abspath(apk2)],
1819 return("Failed to unpack " + apk2)
1821 p = FDroidPopen(['diff', '-r', 'this_apk', 'that_apk'], cwd=tmp_dir,
1823 lines = p.output.splitlines()
1824 if len(lines) != 1 or 'META-INF' not in lines[0]:
1825 return("Unexpected diff output - " + p.output)
1827 # If we get here, it seems like they're the same!