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 '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 logging.error("'sdk_path' not set in config.py!")
204 if thisconfig['sdk_path'] == default_config['sdk_path']:
205 logging.error('No Android SDK found!')
206 logging.error('You can use ANDROID_HOME to set the path to your SDK, i.e.:')
207 logging.error('\texport ANDROID_HOME=/opt/android-sdk')
209 if not os.path.exists(thisconfig['sdk_path']):
210 logging.critical('Android SDK path "' + thisconfig['sdk_path'] + '" does not exist!')
212 if not os.path.isdir(thisconfig['sdk_path']):
213 logging.critical('Android SDK path "' + thisconfig['sdk_path'] + '" is not a directory!')
215 for d in ['build-tools', 'platform-tools', 'tools']:
216 if not os.path.isdir(os.path.join(thisconfig['sdk_path'], d)):
217 logging.critical('Android SDK path "%s" does not contain "%s/"!' % (
218 thisconfig['sdk_path'], d))
223 def ensure_build_tools_exists(thisconfig):
224 if not test_sdk_exists(thisconfig):
226 build_tools = os.path.join(thisconfig['sdk_path'], 'build-tools')
227 versioned_build_tools = os.path.join(build_tools, thisconfig['build_tools'])
228 if not os.path.isdir(versioned_build_tools):
229 logging.critical('Android Build Tools path "'
230 + versioned_build_tools + '" does not exist!')
234 def write_password_file(pwtype, password=None):
236 writes out passwords to a protected file instead of passing passwords as
237 command line argments
239 filename = '.fdroid.' + pwtype + '.txt'
240 fd = os.open(filename, os.O_CREAT | os.O_TRUNC | os.O_WRONLY, 0600)
242 os.write(fd, config[pwtype])
244 os.write(fd, password)
246 config[pwtype + 'file'] = filename
249 # Given the arguments in the form of multiple appid:[vc] strings, this returns
250 # a dictionary with the set of vercodes specified for each package.
251 def read_pkg_args(args, allow_vercodes=False):
258 if allow_vercodes and ':' in p:
259 package, vercode = p.split(':')
261 package, vercode = p, None
262 if package not in vercodes:
263 vercodes[package] = [vercode] if vercode else []
265 elif vercode and vercode not in vercodes[package]:
266 vercodes[package] += [vercode] if vercode else []
271 # On top of what read_pkg_args does, this returns the whole app metadata, but
272 # limiting the builds list to the builds matching the vercodes specified.
273 def read_app_args(args, allapps, allow_vercodes=False):
275 vercodes = read_pkg_args(args, allow_vercodes)
281 for appid, app in allapps.iteritems():
282 if appid in vercodes:
285 if len(apps) != len(vercodes):
288 logging.critical("No such package: %s" % p)
289 raise FDroidException("Found invalid app ids in arguments")
291 raise FDroidException("No packages specified")
294 for appid, app in apps.iteritems():
298 app['builds'] = [b for b in app['builds'] if b['vercode'] in vc]
299 if len(app['builds']) != len(vercodes[appid]):
301 allvcs = [b['vercode'] for b in app['builds']]
302 for v in vercodes[appid]:
304 logging.critical("No such vercode %s for app %s" % (v, appid))
307 raise FDroidException("Found invalid vercodes for some apps")
312 def has_extension(filename, extension):
313 name, ext = os.path.splitext(filename)
314 ext = ext.lower()[1:]
315 return ext == extension
320 def clean_description(description):
321 'Remove unneeded newlines and spaces from a block of description text'
323 # this is split up by paragraph to make removing the newlines easier
324 for paragraph in re.split(r'\n\n', description):
325 paragraph = re.sub('\r', '', paragraph)
326 paragraph = re.sub('\n', ' ', paragraph)
327 paragraph = re.sub(' {2,}', ' ', paragraph)
328 paragraph = re.sub('^\s*(\w)', r'\1', paragraph)
329 returnstring += paragraph + '\n\n'
330 return returnstring.rstrip('\n')
333 def apknameinfo(filename):
335 filename = os.path.basename(filename)
336 if apk_regex is None:
337 apk_regex = re.compile(r"^(.+)_([0-9]+)\.apk$")
338 m = apk_regex.match(filename)
340 result = (m.group(1), m.group(2))
341 except AttributeError:
342 raise FDroidException("Invalid apk name: %s" % filename)
346 def getapkname(app, build):
347 return "%s_%s.apk" % (app['id'], build['vercode'])
350 def getsrcname(app, build):
351 return "%s_%s_src.tar.gz" % (app['id'], build['vercode'])
358 return app['Auto Name']
363 return '%s (%s)' % (app['Current Version'], app['Current Version Code'])
366 def getvcs(vcstype, remote, local):
368 return vcs_git(remote, local)
369 if vcstype == 'git-svn':
370 return vcs_gitsvn(remote, local)
372 return vcs_hg(remote, local)
374 return vcs_bzr(remote, local)
375 if vcstype == 'srclib':
376 if local != os.path.join('build', 'srclib', remote):
377 raise VCSException("Error: srclib paths are hard-coded!")
378 return getsrclib(remote, os.path.join('build', 'srclib'), raw=True)
380 raise VCSException("Deprecated vcs type 'svn' - please use 'git-svn' instead")
381 raise VCSException("Invalid vcs type " + vcstype)
384 def getsrclibvcs(name):
385 if name not in metadata.srclibs:
386 raise VCSException("Missing srclib " + name)
387 return metadata.srclibs[name]['Repo Type']
391 def __init__(self, remote, local):
393 # svn, git-svn and bzr may require auth
395 if self.repotype() in ('git-svn', 'bzr'):
397 self.username, remote = remote.split('@')
398 if ':' not in self.username:
399 raise VCSException("Password required with username")
400 self.username, self.password = self.username.split(':')
404 self.clone_failed = False
405 self.refreshed = False
411 # Take the local repository to a clean version of the given revision, which
412 # is specificed in the VCS's native format. Beforehand, the repository can
413 # be dirty, or even non-existent. If the repository does already exist
414 # locally, it will be updated from the origin, but only once in the
415 # lifetime of the vcs object.
416 # None is acceptable for 'rev' if you know you are cloning a clean copy of
417 # the repo - otherwise it must specify a valid revision.
418 def gotorevision(self, rev):
420 if self.clone_failed:
421 raise VCSException("Downloading the repository already failed once, not trying again.")
423 # The .fdroidvcs-id file for a repo tells us what VCS type
424 # and remote that directory was created from, allowing us to drop it
425 # automatically if either of those things changes.
426 fdpath = os.path.join(self.local, '..',
427 '.fdroidvcs-' + os.path.basename(self.local))
428 cdata = self.repotype() + ' ' + self.remote
431 if os.path.exists(self.local):
432 if os.path.exists(fdpath):
433 with open(fdpath, 'r') as f:
434 fsdata = f.read().strip()
440 "Repository details for %s changed - deleting" % (
444 logging.info("Repository details for %s missing - deleting" % (
447 shutil.rmtree(self.local)
452 self.gotorevisionx(rev)
453 except FDroidException, e:
456 # If necessary, write the .fdroidvcs file.
457 if writeback and not self.clone_failed:
458 with open(fdpath, 'w') as f:
464 # Derived classes need to implement this. It's called once basic checking
465 # has been performend.
466 def gotorevisionx(self, rev):
467 raise VCSException("This VCS type doesn't define gotorevisionx")
469 # Initialise and update submodules
470 def initsubmodules(self):
471 raise VCSException('Submodules not supported for this vcs type')
473 # Get a list of all known tags
475 raise VCSException('gettags not supported for this vcs type')
477 # Get a list of latest number tags
478 def latesttags(self, number):
479 raise VCSException('latesttags not supported for this vcs type')
481 # Get current commit reference (hash, revision, etc)
483 raise VCSException('getref not supported for this vcs type')
485 # Returns the srclib (name, path) used in setting up the current
496 # If the local directory exists, but is somehow not a git repository, git
497 # will traverse up the directory tree until it finds one that is (i.e.
498 # fdroidserver) and then we'll proceed to destroy it! This is called as
501 p = SilentPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local)
502 result = p.output.rstrip()
503 if not result.endswith(self.local):
504 raise VCSException('Repository mismatch')
506 def gotorevisionx(self, rev):
507 if not os.path.exists(self.local):
509 p = FDroidPopen(['git', 'clone', self.remote, self.local])
510 if p.returncode != 0:
511 self.clone_failed = True
512 raise VCSException("Git clone failed", p.output)
516 # Discard any working tree changes
517 p = SilentPopen(['git', 'reset', '--hard'], cwd=self.local)
518 if p.returncode != 0:
519 raise VCSException("Git reset failed", p.output)
520 # Remove untracked files now, in case they're tracked in the target
521 # revision (it happens!)
522 p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
523 if p.returncode != 0:
524 raise VCSException("Git clean failed", p.output)
525 if not self.refreshed:
526 # Get latest commits and tags from remote
527 p = FDroidPopen(['git', 'fetch', 'origin'], cwd=self.local)
528 if p.returncode != 0:
529 raise VCSException("Git fetch failed", p.output)
530 p = SilentPopen(['git', 'fetch', '--prune', '--tags', 'origin'], cwd=self.local)
531 if p.returncode != 0:
532 raise VCSException("Git fetch failed", p.output)
533 # Recreate origin/HEAD as git clone would do it, in case it disappeared
534 p = SilentPopen(['git', 'remote', 'set-head', 'origin', '--auto'], cwd=self.local)
535 if p.returncode != 0:
536 lines = p.output.splitlines()
537 if 'Multiple remote HEAD branches' not in lines[0]:
538 raise VCSException("Git remote set-head failed", p.output)
539 branch = lines[1].split(' ')[-1]
540 p2 = SilentPopen(['git', 'remote', 'set-head', 'origin', branch], cwd=self.local)
541 if p2.returncode != 0:
542 raise VCSException("Git remote set-head failed", p.output + '\n' + p2.output)
543 self.refreshed = True
544 # origin/HEAD is the HEAD of the remote, e.g. the "default branch" on
545 # a github repo. Most of the time this is the same as origin/master.
546 rev = rev or 'origin/HEAD'
547 p = SilentPopen(['git', 'checkout', '-f', rev], cwd=self.local)
548 if p.returncode != 0:
549 raise VCSException("Git checkout of '%s' failed" % rev, p.output)
550 # Get rid of any uncontrolled files left behind
551 p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
552 if p.returncode != 0:
553 raise VCSException("Git clean failed", p.output)
555 def initsubmodules(self):
557 submfile = os.path.join(self.local, '.gitmodules')
558 if not os.path.isfile(submfile):
559 raise VCSException("No git submodules available")
561 # fix submodules not accessible without an account and public key auth
562 with open(submfile, 'r') as f:
563 lines = f.readlines()
564 with open(submfile, 'w') as f:
566 if 'git@github.com' in line:
567 line = line.replace('git@github.com:', 'https://github.com/')
571 ['git', 'reset', '--hard'],
572 ['git', 'clean', '-dffx'],
574 p = SilentPopen(['git', 'submodule', 'foreach', '--recursive'] + cmd, cwd=self.local)
575 if p.returncode != 0:
576 raise VCSException("Git submodule reset failed", p.output)
577 p = SilentPopen(['git', 'submodule', 'sync'], cwd=self.local)
578 if p.returncode != 0:
579 raise VCSException("Git submodule sync failed", p.output)
580 p = FDroidPopen(['git', 'submodule', 'update', '--init', '--force', '--recursive'], cwd=self.local)
581 if p.returncode != 0:
582 raise VCSException("Git submodule update failed", p.output)
586 p = SilentPopen(['git', 'tag'], cwd=self.local)
587 return p.output.splitlines()
589 def latesttags(self, alltags, number):
591 p = SilentPopen(['echo "' + '\n'.join(alltags) + '" | '
592 + 'xargs -I@ git log --format=format:"%at @%n" -1 @ | '
593 + 'sort -n | awk \'{print $2}\''],
594 cwd=self.local, shell=True)
595 return p.output.splitlines()[-number:]
598 class vcs_gitsvn(vcs):
603 # Damn git-svn tries to use a graphical password prompt, so we have to
604 # trick it into taking the password from stdin
606 if self.username is None:
608 return ('echo "%s" | DISPLAY="" ' % self.password, ' --username "%s"' % self.username)
610 # If the local directory exists, but is somehow not a git repository, git
611 # will traverse up the directory tree until it finds one that is (i.e.
612 # fdroidserver) and then we'll proceed to destory it! This is called as
615 p = SilentPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local)
616 result = p.output.rstrip()
617 if not result.endswith(self.local):
618 raise VCSException('Repository mismatch')
620 def gotorevisionx(self, rev):
621 if not os.path.exists(self.local):
623 gitsvn_cmd = '%sgit svn clone%s' % self.userargs()
624 if ';' in self.remote:
625 remote_split = self.remote.split(';')
626 for i in remote_split[1:]:
627 if i.startswith('trunk='):
628 gitsvn_cmd += ' -T %s' % i[6:]
629 elif i.startswith('tags='):
630 gitsvn_cmd += ' -t %s' % i[5:]
631 elif i.startswith('branches='):
632 gitsvn_cmd += ' -b %s' % i[9:]
633 p = SilentPopen([gitsvn_cmd + " %s %s" % (remote_split[0], self.local)], shell=True)
634 if p.returncode != 0:
635 self.clone_failed = True
636 raise VCSException("Git svn clone failed", p.output)
638 p = SilentPopen([gitsvn_cmd + " %s %s" % (self.remote, self.local)], shell=True)
639 if p.returncode != 0:
640 self.clone_failed = True
641 raise VCSException("Git svn clone failed", p.output)
645 # Discard any working tree changes
646 p = SilentPopen(['git', 'reset', '--hard'], cwd=self.local)
647 if p.returncode != 0:
648 raise VCSException("Git reset failed", p.output)
649 # Remove untracked files now, in case they're tracked in the target
650 # revision (it happens!)
651 p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
652 if p.returncode != 0:
653 raise VCSException("Git clean failed", p.output)
654 if not self.refreshed:
655 # Get new commits, branches and tags from repo
656 p = SilentPopen(['%sgit svn fetch %s' % self.userargs()], cwd=self.local, shell=True)
657 if p.returncode != 0:
658 raise VCSException("Git svn fetch failed")
659 p = SilentPopen(['%sgit svn rebase %s' % self.userargs()], cwd=self.local, shell=True)
660 if p.returncode != 0:
661 raise VCSException("Git svn rebase failed", p.output)
662 self.refreshed = True
664 rev = rev or 'master'
666 nospaces_rev = rev.replace(' ', '%20')
667 # Try finding a svn tag
668 for treeish in ['origin/', '']:
669 p = SilentPopen(['git', 'checkout', treeish + 'tags/' + nospaces_rev],
671 if p.returncode == 0:
673 if p.returncode != 0:
674 # No tag found, normal svn rev translation
675 # Translate svn rev into git format
676 rev_split = rev.split('/')
679 for treeish in ['origin/', '']:
680 if len(rev_split) > 1:
681 treeish += rev_split[0]
682 svn_rev = rev_split[1]
685 # if no branch is specified, then assume trunk (i.e. 'master' branch):
689 svn_rev = svn_rev if svn_rev[0] == 'r' else 'r' + svn_rev
691 p = SilentPopen(['git', 'svn', 'find-rev', '--before', svn_rev, treeish],
693 git_rev = p.output.rstrip()
695 if p.returncode == 0 and git_rev:
698 if p.returncode != 0 or not git_rev:
699 # Try a plain git checkout as a last resort
700 p = SilentPopen(['git', 'checkout', rev], cwd=self.local)
701 if p.returncode != 0:
702 raise VCSException("No git treeish found and direct git checkout of '%s' failed" % rev, p.output)
704 # Check out the git rev equivalent to the svn rev
705 p = SilentPopen(['git', 'checkout', git_rev], cwd=self.local)
706 if p.returncode != 0:
707 raise VCSException("Git checkout of '%s' failed" % rev, p.output)
709 # Get rid of any uncontrolled files left behind
710 p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
711 if p.returncode != 0:
712 raise VCSException("Git clean failed", p.output)
716 for treeish in ['origin/', '']:
717 d = os.path.join(self.local, '.git', 'svn', 'refs', 'remotes', treeish, 'tags')
723 p = SilentPopen(['git', 'svn', 'find-rev', 'HEAD'], cwd=self.local)
724 if p.returncode != 0:
726 return p.output.strip()
734 def gotorevisionx(self, rev):
735 if not os.path.exists(self.local):
736 p = SilentPopen(['hg', 'clone', self.remote, self.local])
737 if p.returncode != 0:
738 self.clone_failed = True
739 raise VCSException("Hg clone failed", p.output)
741 p = SilentPopen(['hg status -uS | xargs rm -rf'], cwd=self.local, shell=True)
742 if p.returncode != 0:
743 raise VCSException("Hg clean failed", p.output)
744 if not self.refreshed:
745 p = SilentPopen(['hg', 'pull'], cwd=self.local)
746 if p.returncode != 0:
747 raise VCSException("Hg pull failed", p.output)
748 self.refreshed = True
750 rev = rev or 'default'
753 p = SilentPopen(['hg', 'update', '-C', rev], cwd=self.local)
754 if p.returncode != 0:
755 raise VCSException("Hg checkout of '%s' failed" % rev, p.output)
756 p = SilentPopen(['hg', 'purge', '--all'], cwd=self.local)
757 # Also delete untracked files, we have to enable purge extension for that:
758 if "'purge' is provided by the following extension" in p.output:
759 with open(os.path.join(self.local, '.hg', 'hgrc'), "a") as myfile:
760 myfile.write("\n[extensions]\nhgext.purge=\n")
761 p = SilentPopen(['hg', 'purge', '--all'], cwd=self.local)
762 if p.returncode != 0:
763 raise VCSException("HG purge failed", p.output)
764 elif p.returncode != 0:
765 raise VCSException("HG purge failed", p.output)
768 p = SilentPopen(['hg', 'tags', '-q'], cwd=self.local)
769 return p.output.splitlines()[1:]
777 def gotorevisionx(self, rev):
778 if not os.path.exists(self.local):
779 p = SilentPopen(['bzr', 'branch', self.remote, self.local])
780 if p.returncode != 0:
781 self.clone_failed = True
782 raise VCSException("Bzr branch failed", p.output)
784 p = SilentPopen(['bzr', 'clean-tree', '--force', '--unknown', '--ignored'], cwd=self.local)
785 if p.returncode != 0:
786 raise VCSException("Bzr revert failed", p.output)
787 if not self.refreshed:
788 p = SilentPopen(['bzr', 'pull'], cwd=self.local)
789 if p.returncode != 0:
790 raise VCSException("Bzr update failed", p.output)
791 self.refreshed = True
793 revargs = list(['-r', rev] if rev else [])
794 p = SilentPopen(['bzr', 'revert'] + revargs, cwd=self.local)
795 if p.returncode != 0:
796 raise VCSException("Bzr revert of '%s' failed" % rev, p.output)
799 p = SilentPopen(['bzr', 'tags'], cwd=self.local)
800 return [tag.split(' ')[0].strip() for tag in
801 p.output.splitlines()]
804 def retrieve_string(app_dir, string, xmlfiles=None):
807 os.path.join(app_dir, 'res'),
808 os.path.join(app_dir, 'src', 'main'),
813 for res_dir in res_dirs:
814 for r, d, f in os.walk(res_dir):
815 if os.path.basename(r) == 'values':
816 xmlfiles += [os.path.join(r, x) for x in f if x.endswith('.xml')]
819 if string.startswith('@string/'):
820 string_search = re.compile(r'.*name="' + string[8:] + '".*?>"?([^<]+?)"?<.*').search
821 elif string.startswith('&') and string.endswith(';'):
822 string_search = re.compile(r'.*<!ENTITY.*' + string[1:-1] + '.*?"([^"]+?)".*>').search
824 if string_search is not None:
825 for xmlfile in xmlfiles:
826 for line in file(xmlfile):
827 matches = string_search(line)
829 return retrieve_string(app_dir, matches.group(1), xmlfiles)
832 return string.replace("\\'", "'")
835 # Return list of existing files that will be used to find the highest vercode
836 def manifest_paths(app_dir, flavours):
838 possible_manifests = \
839 [os.path.join(app_dir, 'AndroidManifest.xml'),
840 os.path.join(app_dir, 'src', 'main', 'AndroidManifest.xml'),
841 os.path.join(app_dir, 'src', 'AndroidManifest.xml'),
842 os.path.join(app_dir, 'build.gradle')]
844 for flavour in flavours:
847 possible_manifests.append(
848 os.path.join(app_dir, 'src', flavour, 'AndroidManifest.xml'))
850 return [path for path in possible_manifests if os.path.isfile(path)]
853 # Retrieve the package name. Returns the name, or None if not found.
854 def fetch_real_name(app_dir, flavours):
855 app_search = re.compile(r'.*<application.*').search
856 name_search = re.compile(r'.*android:label="([^"]+)".*').search
858 for f in manifest_paths(app_dir, flavours):
859 if not has_extension(f, 'xml'):
861 logging.debug("fetch_real_name: Checking manifest at " + f)
867 matches = name_search(line)
869 stringname = matches.group(1)
870 logging.debug("fetch_real_name: using string " + stringname)
871 result = retrieve_string(app_dir, stringname)
873 result = result.strip()
878 # Retrieve the version name
879 def version_name(original, app_dir, flavours):
880 for f in manifest_paths(app_dir, flavours):
881 if not has_extension(f, 'xml'):
883 string = retrieve_string(app_dir, original)
889 def get_library_references(root_dir):
891 proppath = os.path.join(root_dir, 'project.properties')
892 if not os.path.isfile(proppath):
894 with open(proppath) as f:
895 for line in f.readlines():
896 if not line.startswith('android.library.reference.'):
898 path = line.split('=')[1].strip()
899 relpath = os.path.join(root_dir, path)
900 if not os.path.isdir(relpath):
902 logging.debug("Found subproject at %s" % path)
903 libraries.append(path)
907 def ant_subprojects(root_dir):
908 subprojects = get_library_references(root_dir)
909 for subpath in subprojects:
910 subrelpath = os.path.join(root_dir, subpath)
911 for p in get_library_references(subrelpath):
912 relp = os.path.normpath(os.path.join(subpath, p))
913 if relp not in subprojects:
914 subprojects.insert(0, relp)
918 def remove_debuggable_flags(root_dir):
919 # Remove forced debuggable flags
920 logging.debug("Removing debuggable flags from %s" % root_dir)
921 for root, dirs, files in os.walk(root_dir):
922 if 'AndroidManifest.xml' in files:
923 path = os.path.join(root, 'AndroidManifest.xml')
924 p = SilentPopen(['sed', '-i', 's/android:debuggable="[^"]*"//g', path])
925 if p.returncode != 0:
926 raise BuildException("Failed to remove debuggable flags of %s" % path)
929 # Extract some information from the AndroidManifest.xml at the given path.
930 # Returns (version, vercode, package), any or all of which might be None.
931 # All values returned are strings.
932 def parse_androidmanifests(paths, ignoreversions=None):
935 return (None, None, None)
937 vcsearch = re.compile(r'.*:versionCode="([0-9]+?)".*').search
938 vnsearch = re.compile(r'.*:versionName="([^"]+?)".*').search
939 psearch = re.compile(r'.*package="([^"]+)".*').search
941 vcsearch_g = re.compile(r'.*versionCode *=* *["\']*([0-9]+)["\']*').search
942 vnsearch_g = re.compile(r'.*versionName *=* *(["\'])((?:(?=(\\?))\3.)*?)\1.*').search
943 psearch_g = re.compile(r'.*packageName *=* *["\']([^"]+)["\'].*').search
945 ignoresearch = re.compile(ignoreversions).search if ignoreversions else None
953 gradle = has_extension(path, 'gradle')
956 # Remember package name, may be defined separately from version+vercode
957 package = max_package
959 for line in file(path):
962 matches = psearch_g(line)
964 matches = psearch(line)
966 package = matches.group(1)
969 matches = vnsearch_g(line)
971 matches = vnsearch(line)
973 version = matches.group(2 if gradle else 1)
976 matches = vcsearch_g(line)
978 matches = vcsearch(line)
980 vercode = matches.group(1)
982 # Always grab the package name and version name in case they are not
983 # together with the highest version code
984 if max_package is None and package is not None:
985 max_package = package
986 if max_version is None and version is not None:
987 max_version = version
989 if max_vercode is None or (vercode is not None and vercode > max_vercode):
990 if not ignoresearch or not ignoresearch(version):
991 if version is not None:
992 max_version = version
993 if vercode is not None:
994 max_vercode = vercode
995 if package is not None:
996 max_package = package
998 max_version = "Ignore"
1000 if max_version is None:
1001 max_version = "Unknown"
1003 return (max_version, max_vercode, max_package)
1006 class FDroidException(Exception):
1007 def __init__(self, value, detail=None):
1009 self.detail = detail
1011 def get_wikitext(self):
1012 ret = repr(self.value) + "\n"
1016 txt = self.detail[-8192:] if len(self.detail) > 8192 else self.detail
1024 ret += "\n==== detail begin ====\n%s\n==== detail end ====" % self.detail.strip()
1028 class VCSException(FDroidException):
1032 class BuildException(FDroidException):
1036 # Get the specified source library.
1037 # Returns the path to it. Normally this is the path to be used when referencing
1038 # it, which may be a subdirectory of the actual project. If you want the base
1039 # directory of the project, pass 'basepath=True'.
1040 def getsrclib(spec, srclib_dir, srclibpaths=[], subdir=None,
1041 basepath=False, raw=False, prepare=True, preponly=False):
1049 name, ref = spec.split('@')
1051 number, name = name.split(':', 1)
1053 name, subdir = name.split('/', 1)
1055 if name not in metadata.srclibs:
1056 raise VCSException('srclib ' + name + ' not found.')
1058 srclib = metadata.srclibs[name]
1060 sdir = os.path.join(srclib_dir, name)
1063 vcs = getvcs(srclib["Repo Type"], srclib["Repo"], sdir)
1064 vcs.srclib = (name, number, sdir)
1066 vcs.gotorevision(ref)
1073 libdir = os.path.join(sdir, subdir)
1074 elif srclib["Subdir"]:
1075 for subdir in srclib["Subdir"]:
1076 libdir_candidate = os.path.join(sdir, subdir)
1077 if os.path.exists(libdir_candidate):
1078 libdir = libdir_candidate
1084 if srclib["Srclibs"]:
1086 for lib in srclib["Srclibs"].replace(';', ',').split(','):
1088 for t in srclibpaths:
1093 raise VCSException('Missing recursive srclib %s for %s' % (
1095 place_srclib(libdir, n, s_tuple[2])
1098 remove_signing_keys(sdir)
1099 remove_debuggable_flags(sdir)
1103 if srclib["Prepare"]:
1104 cmd = replace_config_vars(srclib["Prepare"])
1106 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=libdir)
1107 if p.returncode != 0:
1108 raise BuildException("Error running prepare command for srclib %s"
1114 return (name, number, libdir)
1117 # Prepare the source code for a particular build
1118 # 'vcs' - the appropriate vcs object for the application
1119 # 'app' - the application details from the metadata
1120 # 'build' - the build details from the metadata
1121 # 'build_dir' - the path to the build directory, usually
1123 # 'srclib_dir' - the path to the source libraries directory, usually
1125 # 'extlib_dir' - the path to the external libraries directory, usually
1127 # Returns the (root, srclibpaths) where:
1128 # 'root' is the root directory, which may be the same as 'build_dir' or may
1129 # be a subdirectory of it.
1130 # 'srclibpaths' is information on the srclibs being used
1131 def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=False):
1133 # Optionally, the actual app source can be in a subdirectory
1135 root_dir = os.path.join(build_dir, build['subdir'])
1137 root_dir = build_dir
1139 # Get a working copy of the right revision
1140 logging.info("Getting source for revision " + build['commit'])
1141 vcs.gotorevision(build['commit'])
1143 # Initialise submodules if requred
1144 if build['submodules']:
1145 logging.info("Initialising submodules")
1146 vcs.initsubmodules()
1148 # Check that a subdir (if we're using one) exists. This has to happen
1149 # after the checkout, since it might not exist elsewhere
1150 if not os.path.exists(root_dir):
1151 raise BuildException('Missing subdir ' + root_dir)
1153 # Run an init command if one is required
1155 cmd = replace_config_vars(build['init'])
1156 logging.info("Running 'init' commands in %s" % root_dir)
1158 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
1159 if p.returncode != 0:
1160 raise BuildException("Error running init command for %s:%s" %
1161 (app['id'], build['version']), p.output)
1163 # Apply patches if any
1165 logging.info("Applying patches")
1166 for patch in build['patch']:
1167 patch = patch.strip()
1168 logging.info("Applying " + patch)
1169 patch_path = os.path.join('metadata', app['id'], patch)
1170 p = FDroidPopen(['patch', '-p1', '-i', os.path.abspath(patch_path)], cwd=build_dir)
1171 if p.returncode != 0:
1172 raise BuildException("Failed to apply patch %s" % patch_path)
1174 # Get required source libraries
1176 if build['srclibs']:
1177 logging.info("Collecting source libraries")
1178 for lib in build['srclibs']:
1179 srclibpaths.append(getsrclib(lib, srclib_dir, srclibpaths,
1182 for name, number, libpath in srclibpaths:
1183 place_srclib(root_dir, int(number) if number else None, libpath)
1185 basesrclib = vcs.getsrclib()
1186 # If one was used for the main source, add that too.
1188 srclibpaths.append(basesrclib)
1190 # Update the local.properties file
1191 localprops = [os.path.join(build_dir, 'local.properties')]
1193 localprops += [os.path.join(root_dir, 'local.properties')]
1194 for path in localprops:
1196 if os.path.isfile(path):
1197 logging.info("Updating local.properties file at %s" % path)
1203 logging.info("Creating local.properties file at %s" % path)
1204 # Fix old-fashioned 'sdk-location' by copying
1205 # from sdk.dir, if necessary
1206 if build['oldsdkloc']:
1207 sdkloc = re.match(r".*^sdk.dir=(\S+)$.*", props,
1208 re.S | re.M).group(1)
1209 props += "sdk-location=%s\n" % sdkloc
1211 props += "sdk.dir=%s\n" % config['sdk_path']
1212 props += "sdk-location=%s\n" % config['sdk_path']
1213 if config['ndk_path']:
1215 props += "ndk.dir=%s\n" % config['ndk_path']
1216 props += "ndk-location=%s\n" % config['ndk_path']
1217 # Add java.encoding if necessary
1218 if build['encoding']:
1219 props += "java.encoding=%s\n" % build['encoding']
1225 if build['type'] == 'gradle':
1226 flavours = build['gradle']
1228 version_regex = re.compile(r".*'com\.android\.tools\.build:gradle:([^\.]+\.[^\.]+).*'.*")
1229 gradlepluginver = None
1231 gradle_files = [os.path.join(root_dir, 'build.gradle')]
1233 # Parent dir build.gradle
1234 parent_dir = os.path.normpath(os.path.join(root_dir, '..'))
1235 if parent_dir.startswith(build_dir):
1236 gradle_files.append(os.path.join(parent_dir, 'build.gradle'))
1238 for path in gradle_files:
1241 if not os.path.isfile(path):
1243 with open(path) as f:
1245 match = version_regex.match(line)
1247 gradlepluginver = match.group(1)
1251 build['gradlepluginver'] = LooseVersion(gradlepluginver)
1253 logging.warn("Could not fetch the gradle plugin version, defaulting to 0.11")
1254 build['gradlepluginver'] = LooseVersion('0.11')
1257 n = build["target"].split('-')[1]
1258 SilentPopen(['sed', '-i',
1259 's@compileSdkVersion *[0-9]*@compileSdkVersion ' + n + '@g',
1263 # Remove forced debuggable flags
1264 remove_debuggable_flags(root_dir)
1266 # Insert version code and number into the manifest if necessary
1267 if build['forceversion']:
1268 logging.info("Changing the version name")
1269 for path in manifest_paths(root_dir, flavours):
1270 if not os.path.isfile(path):
1272 if has_extension(path, 'xml'):
1273 p = SilentPopen(['sed', '-i',
1274 's/android:versionName="[^"]*"/android:versionName="'
1275 + build['version'] + '"/g',
1277 if p.returncode != 0:
1278 raise BuildException("Failed to amend manifest")
1279 elif has_extension(path, 'gradle'):
1280 p = SilentPopen(['sed', '-i',
1281 's/versionName *=* *"[^"]*"/versionName = "'
1282 + build['version'] + '"/g',
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 = SilentPopen(['sed', '-i',
1293 's/android:versionCode="[^"]*"/android:versionCode="'
1294 + build['vercode'] + '"/g',
1296 if p.returncode != 0:
1297 raise BuildException("Failed to amend manifest")
1298 elif has_extension(path, 'gradle'):
1299 p = SilentPopen(['sed', '-i',
1300 's/versionCode *=* *[0-9]*/versionCode = '
1301 + build['vercode'] + '/g',
1303 if p.returncode != 0:
1304 raise BuildException("Failed to amend build.gradle")
1306 # Delete unwanted files
1308 logging.info("Removing specified files")
1309 for part in getpaths(build_dir, build, 'rm'):
1310 dest = os.path.join(build_dir, part)
1311 logging.info("Removing {0}".format(part))
1312 if os.path.lexists(dest):
1313 if os.path.islink(dest):
1314 SilentPopen(['unlink ' + dest], shell=True)
1316 SilentPopen(['rm -rf ' + dest], shell=True)
1318 logging.info("...but it didn't exist")
1320 remove_signing_keys(build_dir)
1322 # Add required external libraries
1323 if build['extlibs']:
1324 logging.info("Collecting prebuilt libraries")
1325 libsdir = os.path.join(root_dir, 'libs')
1326 if not os.path.exists(libsdir):
1328 for lib in build['extlibs']:
1330 logging.info("...installing extlib {0}".format(lib))
1331 libf = os.path.basename(lib)
1332 libsrc = os.path.join(extlib_dir, lib)
1333 if not os.path.exists(libsrc):
1334 raise BuildException("Missing extlib file {0}".format(libsrc))
1335 shutil.copyfile(libsrc, os.path.join(libsdir, libf))
1337 # Run a pre-build command if one is required
1338 if build['prebuild']:
1339 logging.info("Running 'prebuild' commands in %s" % root_dir)
1341 cmd = replace_config_vars(build['prebuild'])
1343 # Substitute source library paths into prebuild commands
1344 for name, number, libpath in srclibpaths:
1345 libpath = os.path.relpath(libpath, root_dir)
1346 cmd = cmd.replace('$$' + name + '$$', libpath)
1348 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
1349 if p.returncode != 0:
1350 raise BuildException("Error running prebuild command for %s:%s" %
1351 (app['id'], build['version']), p.output)
1353 # Generate (or update) the ant build file, build.xml...
1354 if build['update'] and build['update'] != ['no'] and build['type'] == 'ant':
1355 parms = ['android', 'update', 'lib-project']
1356 lparms = ['android', 'update', 'project']
1359 parms += ['-t', build['target']]
1360 lparms += ['-t', build['target']]
1361 if build['update'] == ['auto']:
1362 update_dirs = ant_subprojects(root_dir) + ['.']
1364 update_dirs = build['update']
1366 for d in update_dirs:
1367 subdir = os.path.join(root_dir, d)
1369 logging.debug("Updating main project")
1370 cmd = parms + ['-p', d]
1372 logging.debug("Updating subproject %s" % d)
1373 cmd = lparms + ['-p', d]
1374 p = SdkToolsPopen(cmd, cwd=root_dir)
1375 # Check to see whether an error was returned without a proper exit
1376 # code (this is the case for the 'no target set or target invalid'
1378 if p.returncode != 0 or p.output.startswith("Error: "):
1379 raise BuildException("Failed to update project at %s" % d, p.output)
1380 # Clean update dirs via ant
1382 logging.info("Cleaning subproject %s" % d)
1383 p = FDroidPopen(['ant', 'clean'], cwd=subdir)
1385 return (root_dir, srclibpaths)
1388 # Split and extend via globbing the paths from a field
1389 def getpaths(build_dir, build, field):
1391 for p in build[field]:
1393 full_path = os.path.join(build_dir, p)
1394 full_path = os.path.normpath(full_path)
1395 paths += [r[len(build_dir) + 1:] for r in glob.glob(full_path)]
1399 # Scan the source code in the given directory (and all subdirectories)
1400 # and return the number of fatal problems encountered
1401 def scan_source(build_dir, root_dir, thisbuild):
1405 # Common known non-free blobs (always lower case):
1407 re.compile(r'flurryagent', re.IGNORECASE),
1408 re.compile(r'paypal.*mpl', re.IGNORECASE),
1409 re.compile(r'google.*analytics', re.IGNORECASE),
1410 re.compile(r'admob.*sdk.*android', re.IGNORECASE),
1411 re.compile(r'google.*ad.*view', re.IGNORECASE),
1412 re.compile(r'google.*admob', re.IGNORECASE),
1413 re.compile(r'google.*play.*services', re.IGNORECASE),
1414 re.compile(r'crittercism', re.IGNORECASE),
1415 re.compile(r'heyzap', re.IGNORECASE),
1416 re.compile(r'jpct.*ae', re.IGNORECASE),
1417 re.compile(r'youtube.*android.*player.*api', re.IGNORECASE),
1418 re.compile(r'bugsense', re.IGNORECASE),
1419 re.compile(r'crashlytics', re.IGNORECASE),
1420 re.compile(r'ouya.*sdk', re.IGNORECASE),
1421 re.compile(r'libspen23', re.IGNORECASE),
1424 scanignore = getpaths(build_dir, thisbuild, 'scanignore')
1425 scandelete = getpaths(build_dir, thisbuild, 'scandelete')
1428 ms = magic.open(magic.MIME_TYPE)
1430 except AttributeError:
1434 for i in scanignore:
1435 if fd.startswith(i):
1440 for i in scandelete:
1441 if fd.startswith(i):
1445 def removeproblem(what, fd, fp):
1446 logging.info('Removing %s at %s' % (what, fd))
1449 def warnproblem(what, fd):
1450 logging.warn('Found %s at %s' % (what, fd))
1452 def handleproblem(what, fd, fp):
1454 logging.info('Ignoring %s at %s' % (what, fd))
1456 removeproblem(what, fd, fp)
1458 logging.error('Found %s at %s' % (what, fd))
1462 # Iterate through all files in the source code
1463 for r, d, f in os.walk(build_dir, topdown=True):
1465 # It's topdown, so checking the basename is enough
1466 for ignoredir in ('.hg', '.git', '.svn', '.bzr'):
1472 # Path (relative) to the file
1473 fp = os.path.join(r, curfile)
1474 fd = fp[len(build_dir) + 1:]
1477 mime = magic.from_file(fp, mime=True) if ms is None else ms.file(fp)
1478 except UnicodeError:
1479 warnproblem('malformed magic number', fd)
1481 if mime == 'application/x-sharedlib':
1482 count += handleproblem('shared library', fd, fp)
1484 elif mime == 'application/x-archive':
1485 count += handleproblem('static library', fd, fp)
1487 elif mime == 'application/x-executable':
1488 count += handleproblem('binary executable', fd, fp)
1490 elif mime == 'application/x-java-applet':
1491 count += handleproblem('Java compiled class', fd, fp)
1496 'application/java-archive',
1497 'application/octet-stream',
1501 if has_extension(fp, 'apk'):
1502 removeproblem('APK file', fd, fp)
1504 elif has_extension(fp, 'jar'):
1506 if any(suspect.match(curfile) for suspect in usual_suspects):
1507 count += handleproblem('usual supect', fd, fp)
1509 warnproblem('JAR file', fd)
1511 elif has_extension(fp, 'zip'):
1512 warnproblem('ZIP file', fd)
1515 warnproblem('unknown compressed or binary file', fd)
1517 elif has_extension(fp, 'java'):
1518 for line in file(fp):
1519 if 'DexClassLoader' in line:
1520 count += handleproblem('DexClassLoader', fd, fp)
1525 # Presence of a jni directory without buildjni=yes might
1526 # indicate a problem (if it's not a problem, explicitly use
1527 # buildjni=no to bypass this check)
1528 if (os.path.exists(os.path.join(root_dir, 'jni')) and
1529 not thisbuild['buildjni']):
1530 logging.error('Found jni directory, but buildjni is not enabled. Set it to \'no\' to ignore.')
1539 self.path = os.path.join('stats', 'known_apks.txt')
1541 if os.path.exists(self.path):
1542 for line in file(self.path):
1543 t = line.rstrip().split(' ')
1545 self.apks[t[0]] = (t[1], None)
1547 self.apks[t[0]] = (t[1], time.strptime(t[2], '%Y-%m-%d'))
1548 self.changed = False
1550 def writeifchanged(self):
1552 if not os.path.exists('stats'):
1554 f = open(self.path, 'w')
1556 for apk, app in self.apks.iteritems():
1558 line = apk + ' ' + appid
1560 line += ' ' + time.strftime('%Y-%m-%d', added)
1562 for line in sorted(lst):
1563 f.write(line + '\n')
1566 # Record an apk (if it's new, otherwise does nothing)
1567 # Returns the date it was added.
1568 def recordapk(self, apk, app):
1569 if apk not in self.apks:
1570 self.apks[apk] = (app, time.gmtime(time.time()))
1572 _, added = self.apks[apk]
1575 # Look up information - given the 'apkname', returns (app id, date added/None).
1576 # Or returns None for an unknown apk.
1577 def getapp(self, apkname):
1578 if apkname in self.apks:
1579 return self.apks[apkname]
1582 # Get the most recent 'num' apps added to the repo, as a list of package ids
1583 # with the most recent first.
1584 def getlatest(self, num):
1586 for apk, app in self.apks.iteritems():
1590 if apps[appid] > added:
1594 sortedapps = sorted(apps.iteritems(), key=operator.itemgetter(1))[-num:]
1595 lst = [app for app, _ in sortedapps]
1600 def isApkDebuggable(apkfile, config):
1601 """Returns True if the given apk file is debuggable
1603 :param apkfile: full path to the apk to check"""
1605 p = SdkToolsPopen(['aapt', 'dump', 'xmltree', apkfile, 'AndroidManifest.xml'])
1606 if p.returncode != 0:
1607 logging.critical("Failed to get apk manifest information")
1609 for line in p.output.splitlines():
1610 if 'android:debuggable' in line and not line.endswith('0x0'):
1615 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):
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=False)
1652 def SilentPopen(commands, cwd=None, shell=False):
1653 return FDroidPopen(commands, cwd=cwd, shell=shell, output=False)
1656 def FDroidPopen(commands, cwd=None, shell=False, output=True):
1658 Run a command and capture the possibly huge output.
1660 :param commands: command and argument list like in subprocess.Popen
1661 :param cwd: optionally specifies a working directory
1662 :returns: A PopenResult.
1668 cwd = os.path.normpath(cwd)
1669 logging.debug("Directory: %s" % cwd)
1670 logging.debug("> %s" % ' '.join(commands))
1672 result = PopenResult()
1675 p = subprocess.Popen(commands, cwd=cwd, shell=shell, env=env,
1676 stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
1678 raise BuildException("OSError while trying to execute " + ' '.join(commands) + ': ' + str(e))
1680 stdout_queue = Queue.Queue()
1681 stdout_reader = AsynchronousFileReader(p.stdout, stdout_queue)
1682 stdout_reader.start()
1684 # Check the queue for output (until there is no more to get)
1685 while not stdout_reader.eof():
1686 while not stdout_queue.empty():
1687 line = stdout_queue.get()
1688 if output and options.verbose:
1689 # Output directly to console
1690 sys.stderr.write(line)
1692 result.output += line
1696 result.returncode = p.wait()
1700 def remove_signing_keys(build_dir):
1701 comment = re.compile(r'[ ]*//')
1702 signing_configs = re.compile(r'^[\t ]*signingConfigs[ \t]*{[ \t]*$')
1704 re.compile(r'^[\t ]*signingConfig [^ ]*$'),
1705 re.compile(r'.*android\.signingConfigs\.[^{]*$'),
1706 re.compile(r'.*variant\.outputFile = .*'),
1707 re.compile(r'.*\.readLine\(.*'),
1709 for root, dirs, files in os.walk(build_dir):
1710 if 'build.gradle' in files:
1711 path = os.path.join(root, 'build.gradle')
1713 with open(path, "r") as o:
1714 lines = o.readlines()
1719 with open(path, "w") as o:
1721 if comment.match(line):
1725 opened += line.count('{')
1726 opened -= line.count('}')
1729 if signing_configs.match(line):
1734 if any(s.match(line) for s in line_matches):
1742 logging.info("Cleaned build.gradle of keysigning configs at %s" % path)
1745 'project.properties',
1747 'default.properties',
1750 if propfile in files:
1751 path = os.path.join(root, propfile)
1753 with open(path, "r") as o:
1754 lines = o.readlines()
1758 with open(path, "w") as o:
1760 if any(line.startswith(s) for s in ('key.store', 'key.alias')):
1767 logging.info("Cleaned %s of keysigning configs at %s" % (propfile, path))
1770 def replace_config_vars(cmd):
1771 cmd = cmd.replace('$$SDK$$', config['sdk_path'])
1772 cmd = cmd.replace('$$NDK$$', config['ndk_path'])
1773 cmd = cmd.replace('$$MVN3$$', config['mvn3'])
1777 def place_srclib(root_dir, number, libpath):
1780 relpath = os.path.relpath(libpath, root_dir)
1781 proppath = os.path.join(root_dir, 'project.properties')
1784 if os.path.isfile(proppath):
1785 with open(proppath, "r") as o:
1786 lines = o.readlines()
1788 with open(proppath, "w") as o:
1791 if line.startswith('android.library.reference.%d=' % number):
1792 o.write('android.library.reference.%d=%s\n' % (number, relpath))
1797 o.write('android.library.reference.%d=%s\n' % (number, relpath))
1800 def compare_apks(apk1, apk2, tmp_dir):
1803 Returns None if the apk content is the same (apart from the signing key),
1804 otherwise a string describing what's different, or what went wrong when
1805 trying to do the comparison.
1808 thisdir = os.path.join(tmp_dir, 'this_apk')
1809 thatdir = os.path.join(tmp_dir, 'that_apk')
1810 for d in [thisdir, thatdir]:
1811 if os.path.exists(d):
1815 if subprocess.call(['jar', 'xf',
1816 os.path.abspath(apk1)],
1818 return("Failed to unpack " + apk1)
1819 if subprocess.call(['jar', 'xf',
1820 os.path.abspath(apk2)],
1822 return("Failed to unpack " + apk2)
1824 p = FDroidPopen(['diff', '-r', 'this_apk', 'that_apk'], cwd=tmp_dir,
1826 lines = p.output.splitlines()
1827 if len(lines) != 1 or 'META-INF' not in lines[0]:
1828 return("Unexpected diff output - " + p.output)
1830 # If we get here, it seems like they're the same!