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
44 'sdk_path': "$ANDROID_HOME",
47 'r10d': "$ANDROID_NDK"
49 'build_tools': "21.1.2",
53 'sync_from_local_copy_dir': False,
54 'make_current_version_link': True,
55 'current_version_name_source': 'Name',
56 'update_stats': False,
60 'stats_to_carbon': False,
62 'build_server_always': False,
63 'keystore': os.path.join("$HOME", '.local', 'share', 'fdroidserver', 'keystore.jks'),
64 'smartcardoptions': [],
70 'repo_url': "https://MyFirstFDroidRepo.org/fdroid/repo",
71 'repo_name': "My First FDroid Repo Demo",
72 'repo_icon': "fdroid-icon.png",
73 'repo_description': '''
74 This is a repository of apps to be used with FDroid. Applications in this
75 repository are either official binaries built by the original application
76 developers, or are binaries built from source by the admin of f-droid.org
77 using the tools on https://gitlab.com/u/fdroid.
83 def fill_config_defaults(thisconfig):
84 for k, v in default_config.items():
85 if k not in thisconfig:
88 # Expand paths (~users and $vars)
89 def expand_path(path):
93 path = os.path.expanduser(path)
94 path = os.path.expandvars(path)
99 for k in ['sdk_path', 'ant', 'mvn3', 'gradle', 'keystore', 'repo_icon']:
104 thisconfig[k + '_orig'] = v
106 for k in ['ndk_paths']:
112 thisconfig[k][k2] = exp
113 thisconfig[k][k2 + '_orig'] = v
116 def read_config(opts, config_file='config.py'):
117 """Read the repository config
119 The config is read from config_file, which is in the current directory when
120 any of the repo management commands are used.
122 global config, options, env, orig_path
124 if config is not None:
126 if not os.path.isfile(config_file):
127 logging.critical("Missing config file - is this a repo directory?")
134 logging.debug("Reading %s" % config_file)
135 execfile(config_file, config)
137 # smartcardoptions must be a list since its command line args for Popen
138 if 'smartcardoptions' in config:
139 config['smartcardoptions'] = config['smartcardoptions'].split(' ')
140 elif 'keystore' in config and config['keystore'] == 'NONE':
141 # keystore='NONE' means use smartcard, these are required defaults
142 config['smartcardoptions'] = ['-storetype', 'PKCS11', '-providerName',
143 'SunPKCS11-OpenSC', '-providerClass',
144 'sun.security.pkcs11.SunPKCS11',
145 '-providerArg', 'opensc-fdroid.cfg']
147 if any(k in config for k in ["keystore", "keystorepass", "keypass"]):
148 st = os.stat(config_file)
149 if st.st_mode & stat.S_IRWXG or st.st_mode & stat.S_IRWXO:
150 logging.warn("unsafe permissions on {0} (should be 0600)!".format(config_file))
152 fill_config_defaults(config)
154 # There is no standard, so just set up the most common environment
157 orig_path = env['PATH']
158 for n in ['ANDROID_HOME', 'ANDROID_SDK']:
159 env[n] = config['sdk_path']
161 for k in ["keystorepass", "keypass"]:
163 write_password_file(k)
165 for k in ["repo_description", "archive_description"]:
167 config[k] = clean_description(config[k])
169 if 'serverwebroot' in config:
170 if isinstance(config['serverwebroot'], basestring):
171 roots = [config['serverwebroot']]
172 elif all(isinstance(item, basestring) for item in config['serverwebroot']):
173 roots = config['serverwebroot']
175 raise TypeError('only accepts strings, lists, and tuples')
177 for rootstr in roots:
178 # since this is used with rsync, where trailing slashes have
179 # meaning, ensure there is always a trailing slash
180 if rootstr[-1] != '/':
182 rootlist.append(rootstr.replace('//', '/'))
183 config['serverwebroot'] = rootlist
188 def get_ndk_path(version):
190 version = 'r10d' # latest
191 paths = config['ndk_paths']
192 if version not in paths:
194 return paths[version] or ''
197 def find_sdk_tools_cmd(cmd):
198 '''find a working path to a tool from the Android SDK'''
201 if config is not None and 'sdk_path' in config and os.path.exists(config['sdk_path']):
202 # try to find a working path to this command, in all the recent possible paths
203 if 'build_tools' in config:
204 build_tools = os.path.join(config['sdk_path'], 'build-tools')
205 # if 'build_tools' was manually set and exists, check only that one
206 configed_build_tools = os.path.join(build_tools, config['build_tools'])
207 if os.path.exists(configed_build_tools):
208 tooldirs.append(configed_build_tools)
210 # no configed version, so hunt known paths for it
211 for f in sorted(os.listdir(build_tools), reverse=True):
212 if os.path.isdir(os.path.join(build_tools, f)):
213 tooldirs.append(os.path.join(build_tools, f))
214 tooldirs.append(build_tools)
215 sdk_tools = os.path.join(config['sdk_path'], 'tools')
216 if os.path.exists(sdk_tools):
217 tooldirs.append(sdk_tools)
218 sdk_platform_tools = os.path.join(config['sdk_path'], 'platform-tools')
219 if os.path.exists(sdk_platform_tools):
220 tooldirs.append(sdk_platform_tools)
221 tooldirs.append('/usr/bin')
223 if os.path.isfile(os.path.join(d, cmd)):
224 return os.path.join(d, cmd)
225 # did not find the command, exit with error message
226 ensure_build_tools_exists(config)
229 def test_sdk_exists(thisconfig):
230 if 'sdk_path' not in thisconfig:
231 if 'aapt' in thisconfig and os.path.isfile(thisconfig['aapt']):
234 logging.error("'sdk_path' not set in config.py!")
236 if thisconfig['sdk_path'] == default_config['sdk_path']:
237 logging.error('No Android SDK found!')
238 logging.error('You can use ANDROID_HOME to set the path to your SDK, i.e.:')
239 logging.error('\texport ANDROID_HOME=/opt/android-sdk')
241 if not os.path.exists(thisconfig['sdk_path']):
242 logging.critical('Android SDK path "' + thisconfig['sdk_path'] + '" does not exist!')
244 if not os.path.isdir(thisconfig['sdk_path']):
245 logging.critical('Android SDK path "' + thisconfig['sdk_path'] + '" is not a directory!')
247 for d in ['build-tools', 'platform-tools', 'tools']:
248 if not os.path.isdir(os.path.join(thisconfig['sdk_path'], d)):
249 logging.critical('Android SDK path "%s" does not contain "%s/"!' % (
250 thisconfig['sdk_path'], d))
255 def ensure_build_tools_exists(thisconfig):
256 if not test_sdk_exists(thisconfig):
258 build_tools = os.path.join(thisconfig['sdk_path'], 'build-tools')
259 versioned_build_tools = os.path.join(build_tools, thisconfig['build_tools'])
260 if not os.path.isdir(versioned_build_tools):
261 logging.critical('Android Build Tools path "'
262 + versioned_build_tools + '" does not exist!')
266 def write_password_file(pwtype, password=None):
268 writes out passwords to a protected file instead of passing passwords as
269 command line argments
271 filename = '.fdroid.' + pwtype + '.txt'
272 fd = os.open(filename, os.O_CREAT | os.O_TRUNC | os.O_WRONLY, 0600)
274 os.write(fd, config[pwtype])
276 os.write(fd, password)
278 config[pwtype + 'file'] = filename
281 # Given the arguments in the form of multiple appid:[vc] strings, this returns
282 # a dictionary with the set of vercodes specified for each package.
283 def read_pkg_args(args, allow_vercodes=False):
290 if allow_vercodes and ':' in p:
291 package, vercode = p.split(':')
293 package, vercode = p, None
294 if package not in vercodes:
295 vercodes[package] = [vercode] if vercode else []
297 elif vercode and vercode not in vercodes[package]:
298 vercodes[package] += [vercode] if vercode else []
303 # On top of what read_pkg_args does, this returns the whole app metadata, but
304 # limiting the builds list to the builds matching the vercodes specified.
305 def read_app_args(args, allapps, allow_vercodes=False):
307 vercodes = read_pkg_args(args, allow_vercodes)
313 for appid, app in allapps.iteritems():
314 if appid in vercodes:
317 if len(apps) != len(vercodes):
320 logging.critical("No such package: %s" % p)
321 raise FDroidException("Found invalid app ids in arguments")
323 raise FDroidException("No packages specified")
326 for appid, app in apps.iteritems():
330 app['builds'] = [b for b in app['builds'] if b['vercode'] in vc]
331 if len(app['builds']) != len(vercodes[appid]):
333 allvcs = [b['vercode'] for b in app['builds']]
334 for v in vercodes[appid]:
336 logging.critical("No such vercode %s for app %s" % (v, appid))
339 raise FDroidException("Found invalid vercodes for some apps")
344 def has_extension(filename, extension):
345 name, ext = os.path.splitext(filename)
346 ext = ext.lower()[1:]
347 return ext == extension
352 def clean_description(description):
353 'Remove unneeded newlines and spaces from a block of description text'
355 # this is split up by paragraph to make removing the newlines easier
356 for paragraph in re.split(r'\n\n', description):
357 paragraph = re.sub('\r', '', paragraph)
358 paragraph = re.sub('\n', ' ', paragraph)
359 paragraph = re.sub(' {2,}', ' ', paragraph)
360 paragraph = re.sub('^\s*(\w)', r'\1', paragraph)
361 returnstring += paragraph + '\n\n'
362 return returnstring.rstrip('\n')
365 def apknameinfo(filename):
367 filename = os.path.basename(filename)
368 if apk_regex is None:
369 apk_regex = re.compile(r"^(.+)_([0-9]+)\.apk$")
370 m = apk_regex.match(filename)
372 result = (m.group(1), m.group(2))
373 except AttributeError:
374 raise FDroidException("Invalid apk name: %s" % filename)
378 def getapkname(app, build):
379 return "%s_%s.apk" % (app['id'], build['vercode'])
382 def getsrcname(app, build):
383 return "%s_%s_src.tar.gz" % (app['id'], build['vercode'])
390 return app['Auto Name']
395 return '%s (%s)' % (app['Current Version'], app['Current Version Code'])
398 def getvcs(vcstype, remote, local):
400 return vcs_git(remote, local)
401 if vcstype == 'git-svn':
402 return vcs_gitsvn(remote, local)
404 return vcs_hg(remote, local)
406 return vcs_bzr(remote, local)
407 if vcstype == 'srclib':
408 if local != os.path.join('build', 'srclib', remote):
409 raise VCSException("Error: srclib paths are hard-coded!")
410 return getsrclib(remote, os.path.join('build', 'srclib'), raw=True)
412 raise VCSException("Deprecated vcs type 'svn' - please use 'git-svn' instead")
413 raise VCSException("Invalid vcs type " + vcstype)
416 def getsrclibvcs(name):
417 if name not in metadata.srclibs:
418 raise VCSException("Missing srclib " + name)
419 return metadata.srclibs[name]['Repo Type']
424 def __init__(self, remote, local):
426 # svn, git-svn and bzr may require auth
428 if self.repotype() in ('git-svn', 'bzr'):
430 self.username, remote = remote.split('@')
431 if ':' not in self.username:
432 raise VCSException("Password required with username")
433 self.username, self.password = self.username.split(':')
437 self.clone_failed = False
438 self.refreshed = False
444 # Take the local repository to a clean version of the given revision, which
445 # is specificed in the VCS's native format. Beforehand, the repository can
446 # be dirty, or even non-existent. If the repository does already exist
447 # locally, it will be updated from the origin, but only once in the
448 # lifetime of the vcs object.
449 # None is acceptable for 'rev' if you know you are cloning a clean copy of
450 # the repo - otherwise it must specify a valid revision.
451 def gotorevision(self, rev):
453 if self.clone_failed:
454 raise VCSException("Downloading the repository already failed once, not trying again.")
456 # The .fdroidvcs-id file for a repo tells us what VCS type
457 # and remote that directory was created from, allowing us to drop it
458 # automatically if either of those things changes.
459 fdpath = os.path.join(self.local, '..',
460 '.fdroidvcs-' + os.path.basename(self.local))
461 cdata = self.repotype() + ' ' + self.remote
464 if os.path.exists(self.local):
465 if os.path.exists(fdpath):
466 with open(fdpath, 'r') as f:
467 fsdata = f.read().strip()
472 logging.info("Repository details for %s changed - deleting" % (
476 logging.info("Repository details for %s missing - deleting" % (
479 shutil.rmtree(self.local)
484 self.gotorevisionx(rev)
485 except FDroidException, e:
488 # If necessary, write the .fdroidvcs file.
489 if writeback and not self.clone_failed:
490 with open(fdpath, 'w') as f:
496 # Derived classes need to implement this. It's called once basic checking
497 # has been performend.
498 def gotorevisionx(self, rev):
499 raise VCSException("This VCS type doesn't define gotorevisionx")
501 # Initialise and update submodules
502 def initsubmodules(self):
503 raise VCSException('Submodules not supported for this vcs type')
505 # Get a list of all known tags
507 raise VCSException('gettags not supported for this vcs type')
509 # Get a list of latest number tags
510 def latesttags(self, number):
511 raise VCSException('latesttags not supported for this vcs type')
513 # Get current commit reference (hash, revision, etc)
515 raise VCSException('getref not supported for this vcs type')
517 # Returns the srclib (name, path) used in setting up the current
528 # If the local directory exists, but is somehow not a git repository, git
529 # will traverse up the directory tree until it finds one that is (i.e.
530 # fdroidserver) and then we'll proceed to destroy it! This is called as
533 p = FDroidPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local, output=False)
534 result = p.output.rstrip()
535 if not result.endswith(self.local):
536 raise VCSException('Repository mismatch')
538 def gotorevisionx(self, rev):
539 if not os.path.exists(self.local):
541 p = FDroidPopen(['git', 'clone', self.remote, self.local])
542 if p.returncode != 0:
543 self.clone_failed = True
544 raise VCSException("Git clone failed", p.output)
548 # Discard any working tree changes
549 p = FDroidPopen(['git', 'reset', '--hard'], cwd=self.local, output=False)
550 if p.returncode != 0:
551 raise VCSException("Git reset failed", p.output)
552 # Remove untracked files now, in case they're tracked in the target
553 # revision (it happens!)
554 p = FDroidPopen(['git', 'clean', '-dffx'], cwd=self.local, output=False)
555 if p.returncode != 0:
556 raise VCSException("Git clean failed", p.output)
557 if not self.refreshed:
558 # Get latest commits and tags from remote
559 p = FDroidPopen(['git', 'fetch', 'origin'], cwd=self.local)
560 if p.returncode != 0:
561 raise VCSException("Git fetch failed", p.output)
562 p = FDroidPopen(['git', 'fetch', '--prune', '--tags', 'origin'], cwd=self.local, output=False)
563 if p.returncode != 0:
564 raise VCSException("Git fetch failed", p.output)
565 # Recreate origin/HEAD as git clone would do it, in case it disappeared
566 p = FDroidPopen(['git', 'remote', 'set-head', 'origin', '--auto'], cwd=self.local, output=False)
567 if p.returncode != 0:
568 lines = p.output.splitlines()
569 if 'Multiple remote HEAD branches' not in lines[0]:
570 raise VCSException("Git remote set-head failed", p.output)
571 branch = lines[1].split(' ')[-1]
572 p2 = FDroidPopen(['git', 'remote', 'set-head', 'origin', branch], cwd=self.local, output=False)
573 if p2.returncode != 0:
574 raise VCSException("Git remote set-head failed", p.output + '\n' + p2.output)
575 self.refreshed = True
576 # origin/HEAD is the HEAD of the remote, e.g. the "default branch" on
577 # a github repo. Most of the time this is the same as origin/master.
578 rev = rev or 'origin/HEAD'
579 p = FDroidPopen(['git', 'checkout', '-f', rev], cwd=self.local, output=False)
580 if p.returncode != 0:
581 raise VCSException("Git checkout of '%s' failed" % rev, p.output)
582 # Get rid of any uncontrolled files left behind
583 p = FDroidPopen(['git', 'clean', '-dffx'], cwd=self.local, output=False)
584 if p.returncode != 0:
585 raise VCSException("Git clean failed", p.output)
587 def initsubmodules(self):
589 submfile = os.path.join(self.local, '.gitmodules')
590 if not os.path.isfile(submfile):
591 raise VCSException("No git submodules available")
593 # fix submodules not accessible without an account and public key auth
594 with open(submfile, 'r') as f:
595 lines = f.readlines()
596 with open(submfile, 'w') as f:
598 if 'git@github.com' in line:
599 line = line.replace('git@github.com:', 'https://github.com/')
603 ['git', 'reset', '--hard'],
604 ['git', 'clean', '-dffx'],
606 p = FDroidPopen(['git', 'submodule', 'foreach', '--recursive'] + cmd, cwd=self.local, output=False)
607 if p.returncode != 0:
608 raise VCSException("Git submodule reset failed", p.output)
609 p = FDroidPopen(['git', 'submodule', 'sync'], cwd=self.local, output=False)
610 if p.returncode != 0:
611 raise VCSException("Git submodule sync failed", p.output)
612 p = FDroidPopen(['git', 'submodule', 'update', '--init', '--force', '--recursive'], cwd=self.local)
613 if p.returncode != 0:
614 raise VCSException("Git submodule update failed", p.output)
618 p = FDroidPopen(['git', 'tag'], cwd=self.local, output=False)
619 return p.output.splitlines()
621 def latesttags(self, alltags, number):
623 p = FDroidPopen(['echo "' + '\n'.join(alltags) + '" | '
625 'xargs -I@ git log --format=format:"%at @%n" -1 @ | '
626 + 'sort -n | awk \'{print $2}\''],
627 cwd=self.local, shell=True, output=False)
628 return p.output.splitlines()[-number:]
631 class vcs_gitsvn(vcs):
636 # Damn git-svn tries to use a graphical password prompt, so we have to
637 # trick it into taking the password from stdin
639 if self.username is None:
641 return ('echo "%s" | DISPLAY="" ' % self.password, ' --username "%s"' % self.username)
643 # If the local directory exists, but is somehow not a git repository, git
644 # will traverse up the directory tree until it finds one that is (i.e.
645 # fdroidserver) and then we'll proceed to destory it! This is called as
648 p = FDroidPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local, output=False)
649 result = p.output.rstrip()
650 if not result.endswith(self.local):
651 raise VCSException('Repository mismatch')
653 def gotorevisionx(self, rev):
654 if not os.path.exists(self.local):
656 gitsvn_cmd = '%sgit svn clone%s' % self.userargs()
657 if ';' in self.remote:
658 remote_split = self.remote.split(';')
659 for i in remote_split[1:]:
660 if i.startswith('trunk='):
661 gitsvn_cmd += ' -T %s' % i[6:]
662 elif i.startswith('tags='):
663 gitsvn_cmd += ' -t %s' % i[5:]
664 elif i.startswith('branches='):
665 gitsvn_cmd += ' -b %s' % i[9:]
666 p = FDroidPopen([gitsvn_cmd + " %s %s" % (remote_split[0], self.local)], shell=True, output=False)
667 if p.returncode != 0:
668 self.clone_failed = True
669 raise VCSException("Git svn clone failed", p.output)
671 p = FDroidPopen([gitsvn_cmd + " %s %s" % (self.remote, self.local)], shell=True, output=False)
672 if p.returncode != 0:
673 self.clone_failed = True
674 raise VCSException("Git svn clone failed", p.output)
678 # Discard any working tree changes
679 p = FDroidPopen(['git', 'reset', '--hard'], cwd=self.local, output=False)
680 if p.returncode != 0:
681 raise VCSException("Git reset failed", p.output)
682 # Remove untracked files now, in case they're tracked in the target
683 # revision (it happens!)
684 p = FDroidPopen(['git', 'clean', '-dffx'], cwd=self.local, output=False)
685 if p.returncode != 0:
686 raise VCSException("Git clean failed", p.output)
687 if not self.refreshed:
688 # Get new commits, branches and tags from repo
689 p = FDroidPopen(['%sgit svn fetch %s' % self.userargs()], cwd=self.local, shell=True, output=False)
690 if p.returncode != 0:
691 raise VCSException("Git svn fetch failed")
692 p = FDroidPopen(['%sgit svn rebase %s' % self.userargs()], cwd=self.local, shell=True, output=False)
693 if p.returncode != 0:
694 raise VCSException("Git svn rebase failed", p.output)
695 self.refreshed = True
697 rev = rev or 'master'
699 nospaces_rev = rev.replace(' ', '%20')
700 # Try finding a svn tag
701 for treeish in ['origin/', '']:
702 p = FDroidPopen(['git', 'checkout', treeish + 'tags/' + nospaces_rev], cwd=self.local, output=False)
703 if p.returncode == 0:
705 if p.returncode != 0:
706 # No tag found, normal svn rev translation
707 # Translate svn rev into git format
708 rev_split = rev.split('/')
711 for treeish in ['origin/', '']:
712 if len(rev_split) > 1:
713 treeish += rev_split[0]
714 svn_rev = rev_split[1]
717 # if no branch is specified, then assume trunk (i.e. 'master' branch):
721 svn_rev = svn_rev if svn_rev[0] == 'r' else 'r' + svn_rev
723 p = FDroidPopen(['git', 'svn', 'find-rev', '--before', svn_rev, treeish], cwd=self.local, output=False)
724 git_rev = p.output.rstrip()
726 if p.returncode == 0 and git_rev:
729 if p.returncode != 0 or not git_rev:
730 # Try a plain git checkout as a last resort
731 p = FDroidPopen(['git', 'checkout', rev], cwd=self.local, output=False)
732 if p.returncode != 0:
733 raise VCSException("No git treeish found and direct git checkout of '%s' failed" % rev, p.output)
735 # Check out the git rev equivalent to the svn rev
736 p = FDroidPopen(['git', 'checkout', git_rev], cwd=self.local, output=False)
737 if p.returncode != 0:
738 raise VCSException("Git checkout of '%s' failed" % rev, p.output)
740 # Get rid of any uncontrolled files left behind
741 p = FDroidPopen(['git', 'clean', '-dffx'], cwd=self.local, output=False)
742 if p.returncode != 0:
743 raise VCSException("Git clean failed", p.output)
747 for treeish in ['origin/', '']:
748 d = os.path.join(self.local, '.git', 'svn', 'refs', 'remotes', treeish, 'tags')
754 p = FDroidPopen(['git', 'svn', 'find-rev', 'HEAD'], cwd=self.local, output=False)
755 if p.returncode != 0:
757 return p.output.strip()
765 def gotorevisionx(self, rev):
766 if not os.path.exists(self.local):
767 p = FDroidPopen(['hg', 'clone', self.remote, self.local], output=False)
768 if p.returncode != 0:
769 self.clone_failed = True
770 raise VCSException("Hg clone failed", p.output)
772 p = FDroidPopen(['hg status -uS | xargs rm -rf'], cwd=self.local, shell=True, output=False)
773 if p.returncode != 0:
774 raise VCSException("Hg clean failed", p.output)
775 if not self.refreshed:
776 p = FDroidPopen(['hg', 'pull'], cwd=self.local, output=False)
777 if p.returncode != 0:
778 raise VCSException("Hg pull failed", p.output)
779 self.refreshed = True
781 rev = rev or 'default'
784 p = FDroidPopen(['hg', 'update', '-C', rev], cwd=self.local, output=False)
785 if p.returncode != 0:
786 raise VCSException("Hg checkout of '%s' failed" % rev, p.output)
787 p = FDroidPopen(['hg', 'purge', '--all'], cwd=self.local, output=False)
788 # Also delete untracked files, we have to enable purge extension for that:
789 if "'purge' is provided by the following extension" in p.output:
790 with open(os.path.join(self.local, '.hg', 'hgrc'), "a") as myfile:
791 myfile.write("\n[extensions]\nhgext.purge=\n")
792 p = FDroidPopen(['hg', 'purge', '--all'], cwd=self.local, output=False)
793 if p.returncode != 0:
794 raise VCSException("HG purge failed", p.output)
795 elif p.returncode != 0:
796 raise VCSException("HG purge failed", p.output)
799 p = FDroidPopen(['hg', 'tags', '-q'], cwd=self.local, output=False)
800 return p.output.splitlines()[1:]
808 def gotorevisionx(self, rev):
809 if not os.path.exists(self.local):
810 p = FDroidPopen(['bzr', 'branch', self.remote, self.local], output=False)
811 if p.returncode != 0:
812 self.clone_failed = True
813 raise VCSException("Bzr branch failed", p.output)
815 p = FDroidPopen(['bzr', 'clean-tree', '--force', '--unknown', '--ignored'], cwd=self.local, output=False)
816 if p.returncode != 0:
817 raise VCSException("Bzr revert failed", p.output)
818 if not self.refreshed:
819 p = FDroidPopen(['bzr', 'pull'], cwd=self.local, output=False)
820 if p.returncode != 0:
821 raise VCSException("Bzr update failed", p.output)
822 self.refreshed = True
824 revargs = list(['-r', rev] if rev else [])
825 p = FDroidPopen(['bzr', 'revert'] + revargs, cwd=self.local, output=False)
826 if p.returncode != 0:
827 raise VCSException("Bzr revert of '%s' failed" % rev, p.output)
830 p = FDroidPopen(['bzr', 'tags'], cwd=self.local, output=False)
831 return [tag.split(' ')[0].strip() for tag in
832 p.output.splitlines()]
835 def retrieve_string(app_dir, string, xmlfiles=None):
838 os.path.join(app_dir, 'res'),
839 os.path.join(app_dir, 'src', 'main'),
844 for res_dir in res_dirs:
845 for r, d, f in os.walk(res_dir):
846 if os.path.basename(r) == 'values':
847 xmlfiles += [os.path.join(r, x) for x in f if x.endswith('.xml')]
850 if string.startswith('@string/'):
851 string_search = re.compile(r'.*name="' + string[8:] + '".*?>"?([^<]+?)"?<.*').search
852 elif string.startswith('&') and string.endswith(';'):
853 string_search = re.compile(r'.*<!ENTITY.*' + string[1:-1] + '.*?"([^"]+?)".*>').search
855 if string_search is not None:
856 for xmlfile in xmlfiles:
857 for line in file(xmlfile):
858 matches = string_search(line)
860 return retrieve_string(app_dir, matches.group(1), xmlfiles)
863 return string.replace("\\'", "'")
866 # Return list of existing files that will be used to find the highest vercode
867 def manifest_paths(app_dir, flavours):
869 possible_manifests = \
870 [os.path.join(app_dir, 'AndroidManifest.xml'),
871 os.path.join(app_dir, 'src', 'main', 'AndroidManifest.xml'),
872 os.path.join(app_dir, 'src', 'AndroidManifest.xml'),
873 os.path.join(app_dir, 'build.gradle')]
875 for flavour in flavours:
878 possible_manifests.append(
879 os.path.join(app_dir, 'src', flavour, 'AndroidManifest.xml'))
881 return [path for path in possible_manifests if os.path.isfile(path)]
884 # Retrieve the package name. Returns the name, or None if not found.
885 def fetch_real_name(app_dir, flavours):
886 app_search = re.compile(r'.*<application.*').search
887 name_search = re.compile(r'.*android:label="([^"]+)".*').search
889 for f in manifest_paths(app_dir, flavours):
890 if not has_extension(f, 'xml'):
892 logging.debug("fetch_real_name: Checking manifest at " + f)
898 matches = name_search(line)
900 stringname = matches.group(1)
901 logging.debug("fetch_real_name: using string " + stringname)
902 result = retrieve_string(app_dir, stringname)
904 result = result.strip()
909 # Retrieve the version name
910 def version_name(original, app_dir, flavours):
911 for f in manifest_paths(app_dir, flavours):
912 if not has_extension(f, 'xml'):
914 string = retrieve_string(app_dir, original)
920 def get_library_references(root_dir):
922 proppath = os.path.join(root_dir, 'project.properties')
923 if not os.path.isfile(proppath):
925 with open(proppath) as f:
926 for line in f.readlines():
927 if not line.startswith('android.library.reference.'):
929 path = line.split('=')[1].strip()
930 relpath = os.path.join(root_dir, path)
931 if not os.path.isdir(relpath):
933 logging.debug("Found subproject at %s" % path)
934 libraries.append(path)
938 def ant_subprojects(root_dir):
939 subprojects = get_library_references(root_dir)
940 for subpath in subprojects:
941 subrelpath = os.path.join(root_dir, subpath)
942 for p in get_library_references(subrelpath):
943 relp = os.path.normpath(os.path.join(subpath, p))
944 if relp not in subprojects:
945 subprojects.insert(0, relp)
949 def remove_debuggable_flags(root_dir):
950 # Remove forced debuggable flags
951 logging.debug("Removing debuggable flags from %s" % root_dir)
952 for root, dirs, files in os.walk(root_dir):
953 if 'AndroidManifest.xml' in files:
954 path = os.path.join(root, 'AndroidManifest.xml')
955 p = FDroidPopen(['sed', '-i', 's/android:debuggable="[^"]*"//g', path], output=False)
956 if p.returncode != 0:
957 raise BuildException("Failed to remove debuggable flags of %s" % path)
960 # Extract some information from the AndroidManifest.xml at the given path.
961 # Returns (version, vercode, package), any or all of which might be None.
962 # All values returned are strings.
963 def parse_androidmanifests(paths, ignoreversions=None):
966 return (None, None, None)
968 vcsearch = re.compile(r'.*:versionCode="([0-9]+?)".*').search
969 vnsearch = re.compile(r'.*:versionName="([^"]+?)".*').search
970 psearch = re.compile(r'.*package="([^"]+)".*').search
972 vcsearch_g = re.compile(r'.*versionCode *=* *["\']*([0-9]+)["\']*').search
973 vnsearch_g = re.compile(r'.*versionName *=* *(["\'])((?:(?=(\\?))\3.)*?)\1.*').search
974 psearch_g = re.compile(r'.*packageName *=* *["\']([^"]+)["\'].*').search
976 ignoresearch = re.compile(ignoreversions).search if ignoreversions else None
984 gradle = has_extension(path, 'gradle')
987 # Remember package name, may be defined separately from version+vercode
988 package = max_package
990 for line in file(path):
993 matches = psearch_g(line)
995 matches = psearch(line)
997 package = matches.group(1)
1000 matches = vnsearch_g(line)
1002 matches = vnsearch(line)
1004 version = matches.group(2 if gradle else 1)
1007 matches = vcsearch_g(line)
1009 matches = vcsearch(line)
1011 vercode = matches.group(1)
1013 # Always grab the package name and version name in case they are not
1014 # together with the highest version code
1015 if max_package is None and package is not None:
1016 max_package = package
1017 if max_version is None and version is not None:
1018 max_version = version
1020 if max_vercode is None or (vercode is not None and vercode > max_vercode):
1021 if not ignoresearch or not ignoresearch(version):
1022 if version is not None:
1023 max_version = version
1024 if vercode is not None:
1025 max_vercode = vercode
1026 if package is not None:
1027 max_package = package
1029 max_version = "Ignore"
1031 if max_version is None:
1032 max_version = "Unknown"
1034 return (max_version, max_vercode, max_package)
1037 class FDroidException(Exception):
1039 def __init__(self, value, detail=None):
1041 self.detail = detail
1043 def get_wikitext(self):
1044 ret = repr(self.value) + "\n"
1048 txt = self.detail[-8192:] if len(self.detail) > 8192 else self.detail
1056 ret += "\n==== detail begin ====\n%s\n==== detail end ====" % self.detail.strip()
1060 class VCSException(FDroidException):
1064 class BuildException(FDroidException):
1068 # Get the specified source library.
1069 # Returns the path to it. Normally this is the path to be used when referencing
1070 # it, which may be a subdirectory of the actual project. If you want the base
1071 # directory of the project, pass 'basepath=True'.
1072 def getsrclib(spec, srclib_dir, srclibpaths=[], subdir=None,
1073 basepath=False, raw=False, prepare=True, preponly=False):
1081 name, ref = spec.split('@')
1083 number, name = name.split(':', 1)
1085 name, subdir = name.split('/', 1)
1087 if name not in metadata.srclibs:
1088 raise VCSException('srclib ' + name + ' not found.')
1090 srclib = metadata.srclibs[name]
1092 sdir = os.path.join(srclib_dir, name)
1095 vcs = getvcs(srclib["Repo Type"], srclib["Repo"], sdir)
1096 vcs.srclib = (name, number, sdir)
1098 vcs.gotorevision(ref)
1105 libdir = os.path.join(sdir, subdir)
1106 elif srclib["Subdir"]:
1107 for subdir in srclib["Subdir"]:
1108 libdir_candidate = os.path.join(sdir, subdir)
1109 if os.path.exists(libdir_candidate):
1110 libdir = libdir_candidate
1116 if srclib["Srclibs"]:
1118 for lib in srclib["Srclibs"].replace(';', ',').split(','):
1120 for t in srclibpaths:
1125 raise VCSException('Missing recursive srclib %s for %s' % (
1127 place_srclib(libdir, n, s_tuple[2])
1130 remove_signing_keys(sdir)
1131 remove_debuggable_flags(sdir)
1135 if srclib["Prepare"]:
1136 cmd = replace_config_vars(srclib["Prepare"])
1138 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=libdir)
1139 if p.returncode != 0:
1140 raise BuildException("Error running prepare command for srclib %s"
1146 return (name, number, libdir)
1149 # Prepare the source code for a particular build
1150 # 'vcs' - the appropriate vcs object for the application
1151 # 'app' - the application details from the metadata
1152 # 'build' - the build details from the metadata
1153 # 'build_dir' - the path to the build directory, usually
1155 # 'srclib_dir' - the path to the source libraries directory, usually
1157 # 'extlib_dir' - the path to the external libraries directory, usually
1159 # Returns the (root, srclibpaths) where:
1160 # 'root' is the root directory, which may be the same as 'build_dir' or may
1161 # be a subdirectory of it.
1162 # 'srclibpaths' is information on the srclibs being used
1163 def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=False):
1165 # Optionally, the actual app source can be in a subdirectory
1167 root_dir = os.path.join(build_dir, build['subdir'])
1169 root_dir = build_dir
1171 # Get a working copy of the right revision
1172 logging.info("Getting source for revision " + build['commit'])
1173 vcs.gotorevision(build['commit'])
1175 # Initialise submodules if requred
1176 if build['submodules']:
1177 logging.info("Initialising submodules")
1178 vcs.initsubmodules()
1180 # Check that a subdir (if we're using one) exists. This has to happen
1181 # after the checkout, since it might not exist elsewhere
1182 if not os.path.exists(root_dir):
1183 raise BuildException('Missing subdir ' + root_dir)
1185 # Run an init command if one is required
1187 cmd = replace_config_vars(build['init'])
1188 logging.info("Running 'init' commands in %s" % root_dir)
1190 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
1191 if p.returncode != 0:
1192 raise BuildException("Error running init command for %s:%s" %
1193 (app['id'], build['version']), p.output)
1195 # Apply patches if any
1197 logging.info("Applying patches")
1198 for patch in build['patch']:
1199 patch = patch.strip()
1200 logging.info("Applying " + patch)
1201 patch_path = os.path.join('metadata', app['id'], patch)
1202 p = FDroidPopen(['patch', '-p1', '-i', os.path.abspath(patch_path)], cwd=build_dir)
1203 if p.returncode != 0:
1204 raise BuildException("Failed to apply patch %s" % patch_path)
1206 # Get required source libraries
1208 if build['srclibs']:
1209 logging.info("Collecting source libraries")
1210 for lib in build['srclibs']:
1211 srclibpaths.append(getsrclib(lib, srclib_dir, build, srclibpaths,
1214 for name, number, libpath in srclibpaths:
1215 place_srclib(root_dir, int(number) if number else None, libpath)
1217 basesrclib = vcs.getsrclib()
1218 # If one was used for the main source, add that too.
1220 srclibpaths.append(basesrclib)
1222 # Update the local.properties file
1223 localprops = [os.path.join(build_dir, 'local.properties')]
1225 localprops += [os.path.join(root_dir, 'local.properties')]
1226 for path in localprops:
1228 if os.path.isfile(path):
1229 logging.info("Updating local.properties file at %s" % path)
1235 logging.info("Creating local.properties file at %s" % path)
1236 # Fix old-fashioned 'sdk-location' by copying
1237 # from sdk.dir, if necessary
1238 if build['oldsdkloc']:
1239 sdkloc = re.match(r".*^sdk.dir=(\S+)$.*", props,
1240 re.S | re.M).group(1)
1241 props += "sdk-location=%s\n" % sdkloc
1243 props += "sdk.dir=%s\n" % config['sdk_path']
1244 props += "sdk-location=%s\n" % config['sdk_path']
1245 if build['ndk_path']:
1247 props += "ndk.dir=%s\n" % build['ndk_path']
1248 props += "ndk-location=%s\n" % build['ndk_path']
1249 # Add java.encoding if necessary
1250 if build['encoding']:
1251 props += "java.encoding=%s\n" % build['encoding']
1257 if build['type'] == 'gradle':
1258 flavours = build['gradle']
1260 version_regex = re.compile(r".*'com\.android\.tools\.build:gradle:([^\.]+\.[^\.]+).*'.*")
1261 gradlepluginver = None
1263 gradle_files = [os.path.join(root_dir, 'build.gradle')]
1265 # Parent dir build.gradle
1266 parent_dir = os.path.normpath(os.path.join(root_dir, '..'))
1267 if parent_dir.startswith(build_dir):
1268 gradle_files.append(os.path.join(parent_dir, 'build.gradle'))
1270 for path in gradle_files:
1273 if not os.path.isfile(path):
1275 with open(path) as f:
1277 match = version_regex.match(line)
1279 gradlepluginver = match.group(1)
1283 build['gradlepluginver'] = LooseVersion(gradlepluginver)
1285 logging.warn("Could not fetch the gradle plugin version, defaulting to 0.11")
1286 build['gradlepluginver'] = LooseVersion('0.11')
1289 n = build["target"].split('-')[1]
1290 FDroidPopen(['sed', '-i',
1291 's@compileSdkVersion *[0-9]*@compileSdkVersion ' + n + '@g',
1292 'build.gradle'], cwd=root_dir, output=False)
1294 # Remove forced debuggable flags
1295 remove_debuggable_flags(root_dir)
1297 # Insert version code and number into the manifest if necessary
1298 if build['forceversion']:
1299 logging.info("Changing the version name")
1300 for path in manifest_paths(root_dir, flavours):
1301 if not os.path.isfile(path):
1303 if has_extension(path, 'xml'):
1304 p = FDroidPopen(['sed', '-i',
1305 's/android:versionName="[^"]*"/android:versionName="' + build['version'] + '"/g',
1306 path], output=False)
1307 if p.returncode != 0:
1308 raise BuildException("Failed to amend manifest")
1309 elif has_extension(path, 'gradle'):
1310 p = FDroidPopen(['sed', '-i',
1311 's/versionName *=* *"[^"]*"/versionName = "' + build['version'] + '"/g',
1312 path], output=False)
1313 if p.returncode != 0:
1314 raise BuildException("Failed to amend build.gradle")
1315 if build['forcevercode']:
1316 logging.info("Changing the version code")
1317 for path in manifest_paths(root_dir, flavours):
1318 if not os.path.isfile(path):
1320 if has_extension(path, 'xml'):
1321 p = FDroidPopen(['sed', '-i',
1322 's/android:versionCode="[^"]*"/android:versionCode="' + build['vercode'] + '"/g',
1323 path], output=False)
1324 if p.returncode != 0:
1325 raise BuildException("Failed to amend manifest")
1326 elif has_extension(path, 'gradle'):
1327 p = FDroidPopen(['sed', '-i',
1328 's/versionCode *=* *[0-9]*/versionCode = ' + build['vercode'] + '/g',
1329 path], output=False)
1330 if p.returncode != 0:
1331 raise BuildException("Failed to amend build.gradle")
1333 # Delete unwanted files
1335 logging.info("Removing specified files")
1336 for part in getpaths(build_dir, build, 'rm'):
1337 dest = os.path.join(build_dir, part)
1338 logging.info("Removing {0}".format(part))
1339 if os.path.lexists(dest):
1340 if os.path.islink(dest):
1341 FDroidPopen(['unlink ' + dest], shell=True, output=False)
1343 FDroidPopen(['rm -rf ' + dest], shell=True, output=False)
1345 logging.info("...but it didn't exist")
1347 remove_signing_keys(build_dir)
1349 # Add required external libraries
1350 if build['extlibs']:
1351 logging.info("Collecting prebuilt libraries")
1352 libsdir = os.path.join(root_dir, 'libs')
1353 if not os.path.exists(libsdir):
1355 for lib in build['extlibs']:
1357 logging.info("...installing extlib {0}".format(lib))
1358 libf = os.path.basename(lib)
1359 libsrc = os.path.join(extlib_dir, lib)
1360 if not os.path.exists(libsrc):
1361 raise BuildException("Missing extlib file {0}".format(libsrc))
1362 shutil.copyfile(libsrc, os.path.join(libsdir, libf))
1364 # Run a pre-build command if one is required
1365 if build['prebuild']:
1366 logging.info("Running 'prebuild' commands in %s" % root_dir)
1368 cmd = replace_config_vars(build['prebuild'])
1370 # Substitute source library paths into prebuild commands
1371 for name, number, libpath in srclibpaths:
1372 libpath = os.path.relpath(libpath, root_dir)
1373 cmd = cmd.replace('$$' + name + '$$', libpath)
1375 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
1376 if p.returncode != 0:
1377 raise BuildException("Error running prebuild command for %s:%s" %
1378 (app['id'], build['version']), p.output)
1380 # Generate (or update) the ant build file, build.xml...
1381 if build['update'] and build['update'] != ['no'] and build['type'] == 'ant':
1382 parms = ['android', 'update', 'lib-project']
1383 lparms = ['android', 'update', 'project']
1386 parms += ['-t', build['target']]
1387 lparms += ['-t', build['target']]
1388 if build['update'] == ['auto']:
1389 update_dirs = ant_subprojects(root_dir) + ['.']
1391 update_dirs = build['update']
1393 for d in update_dirs:
1394 subdir = os.path.join(root_dir, d)
1396 logging.debug("Updating main project")
1397 cmd = parms + ['-p', d]
1399 logging.debug("Updating subproject %s" % d)
1400 cmd = lparms + ['-p', d]
1401 p = SdkToolsPopen(cmd, cwd=root_dir)
1402 # Check to see whether an error was returned without a proper exit
1403 # code (this is the case for the 'no target set or target invalid'
1405 if p.returncode != 0 or p.output.startswith("Error: "):
1406 raise BuildException("Failed to update project at %s" % d, p.output)
1407 # Clean update dirs via ant
1409 logging.info("Cleaning subproject %s" % d)
1410 p = FDroidPopen(['ant', 'clean'], cwd=subdir)
1412 return (root_dir, srclibpaths)
1415 # Split and extend via globbing the paths from a field
1416 def getpaths(build_dir, build, field):
1418 for p in build[field]:
1420 full_path = os.path.join(build_dir, p)
1421 full_path = os.path.normpath(full_path)
1422 paths += [r[len(build_dir) + 1:] for r in glob.glob(full_path)]
1426 # Scan the source code in the given directory (and all subdirectories)
1427 # and return the number of fatal problems encountered
1428 def scan_source(build_dir, root_dir, thisbuild):
1432 # Common known non-free blobs (always lower case):
1434 re.compile(r'flurryagent', re.IGNORECASE),
1435 re.compile(r'paypal.*mpl', re.IGNORECASE),
1436 re.compile(r'google.*analytics', re.IGNORECASE),
1437 re.compile(r'admob.*sdk.*android', re.IGNORECASE),
1438 re.compile(r'google.*ad.*view', re.IGNORECASE),
1439 re.compile(r'google.*admob', re.IGNORECASE),
1440 re.compile(r'google.*play.*services', re.IGNORECASE),
1441 re.compile(r'crittercism', re.IGNORECASE),
1442 re.compile(r'heyzap', re.IGNORECASE),
1443 re.compile(r'jpct.*ae', re.IGNORECASE),
1444 re.compile(r'youtube.*android.*player.*api', re.IGNORECASE),
1445 re.compile(r'bugsense', re.IGNORECASE),
1446 re.compile(r'crashlytics', re.IGNORECASE),
1447 re.compile(r'ouya.*sdk', re.IGNORECASE),
1448 re.compile(r'libspen23', re.IGNORECASE),
1451 scanignore = getpaths(build_dir, thisbuild, 'scanignore')
1452 scandelete = getpaths(build_dir, thisbuild, 'scandelete')
1454 scanignore_worked = set()
1455 scandelete_worked = set()
1458 ms = magic.open(magic.MIME_TYPE)
1460 except AttributeError:
1464 for p in scanignore:
1465 if fd.startswith(p):
1466 scanignore_worked.add(p)
1471 for p in scandelete:
1472 if fd.startswith(p):
1473 scandelete_worked.add(p)
1477 def ignoreproblem(what, fd, fp):
1478 logging.info('Ignoring %s at %s' % (what, fd))
1481 def removeproblem(what, fd, fp):
1482 logging.info('Removing %s at %s' % (what, fd))
1486 def warnproblem(what, fd):
1487 logging.warn('Found %s at %s' % (what, fd))
1489 def handleproblem(what, fd, fp):
1491 return ignoreproblem(what, fd, fp)
1493 return removeproblem(what, fd, fp)
1494 logging.error('Found %s at %s' % (what, fd))
1497 # Iterate through all files in the source code
1498 for r, d, f in os.walk(build_dir, topdown=True):
1500 # It's topdown, so checking the basename is enough
1501 for ignoredir in ('.hg', '.git', '.svn', '.bzr'):
1507 # Path (relative) to the file
1508 fp = os.path.join(r, curfile)
1509 fd = fp[len(build_dir) + 1:]
1512 mime = magic.from_file(fp, mime=True) if ms is None else ms.file(fp)
1513 except UnicodeError:
1514 warnproblem('malformed magic number', fd)
1516 if mime == 'application/x-sharedlib':
1517 count += handleproblem('shared library', fd, fp)
1519 elif mime == 'application/x-archive':
1520 count += handleproblem('static library', fd, fp)
1522 elif mime == 'application/x-executable':
1523 count += handleproblem('binary executable', fd, fp)
1525 elif mime == 'application/x-java-applet':
1526 count += handleproblem('Java compiled class', fd, fp)
1531 'application/java-archive',
1532 'application/octet-stream',
1536 if has_extension(fp, 'apk'):
1537 removeproblem('APK file', fd, fp)
1539 elif has_extension(fp, 'jar'):
1541 if any(suspect.match(curfile) for suspect in usual_suspects):
1542 count += handleproblem('usual supect', fd, fp)
1544 warnproblem('JAR file', fd)
1546 elif has_extension(fp, 'zip'):
1547 warnproblem('ZIP file', fd)
1550 warnproblem('unknown compressed or binary file', fd)
1552 elif has_extension(fp, 'java'):
1553 for line in file(fp):
1554 if 'DexClassLoader' in line:
1555 count += handleproblem('DexClassLoader', fd, fp)
1560 for p in scanignore:
1561 if p not in scanignore_worked:
1562 logging.error('Unused scanignore path: %s' % p)
1565 for p in scandelete:
1566 if p not in scandelete_worked:
1567 logging.error('Unused scandelete path: %s' % p)
1570 # Presence of a jni directory without buildjni=yes might
1571 # indicate a problem (if it's not a problem, explicitly use
1572 # buildjni=no to bypass this check)
1573 if (os.path.exists(os.path.join(root_dir, 'jni')) and
1574 not thisbuild['buildjni']):
1575 logging.error('Found jni directory, but buildjni is not enabled. Set it to \'no\' to ignore.')
1584 self.path = os.path.join('stats', 'known_apks.txt')
1586 if os.path.exists(self.path):
1587 for line in file(self.path):
1588 t = line.rstrip().split(' ')
1590 self.apks[t[0]] = (t[1], None)
1592 self.apks[t[0]] = (t[1], time.strptime(t[2], '%Y-%m-%d'))
1593 self.changed = False
1595 def writeifchanged(self):
1597 if not os.path.exists('stats'):
1599 f = open(self.path, 'w')
1601 for apk, app in self.apks.iteritems():
1603 line = apk + ' ' + appid
1605 line += ' ' + time.strftime('%Y-%m-%d', added)
1607 for line in sorted(lst):
1608 f.write(line + '\n')
1611 # Record an apk (if it's new, otherwise does nothing)
1612 # Returns the date it was added.
1613 def recordapk(self, apk, app):
1614 if apk not in self.apks:
1615 self.apks[apk] = (app, time.gmtime(time.time()))
1617 _, added = self.apks[apk]
1620 # Look up information - given the 'apkname', returns (app id, date added/None).
1621 # Or returns None for an unknown apk.
1622 def getapp(self, apkname):
1623 if apkname in self.apks:
1624 return self.apks[apkname]
1627 # Get the most recent 'num' apps added to the repo, as a list of package ids
1628 # with the most recent first.
1629 def getlatest(self, num):
1631 for apk, app in self.apks.iteritems():
1635 if apps[appid] > added:
1639 sortedapps = sorted(apps.iteritems(), key=operator.itemgetter(1))[-num:]
1640 lst = [app for app, _ in sortedapps]
1645 def isApkDebuggable(apkfile, config):
1646 """Returns True if the given apk file is debuggable
1648 :param apkfile: full path to the apk to check"""
1650 p = SdkToolsPopen(['aapt', 'dump', 'xmltree', apkfile, 'AndroidManifest.xml'],
1652 if p.returncode != 0:
1653 logging.critical("Failed to get apk manifest information")
1655 for line in p.output.splitlines():
1656 if 'android:debuggable' in line and not line.endswith('0x0'):
1661 class AsynchronousFileReader(threading.Thread):
1664 Helper class to implement asynchronous reading of a file
1665 in a separate thread. Pushes read lines on a queue to
1666 be consumed in another thread.
1669 def __init__(self, fd, queue):
1670 assert isinstance(queue, Queue.Queue)
1671 assert callable(fd.readline)
1672 threading.Thread.__init__(self)
1677 '''The body of the tread: read lines and put them on the queue.'''
1678 for line in iter(self._fd.readline, ''):
1679 self._queue.put(line)
1682 '''Check whether there is no more content to expect.'''
1683 return not self.is_alive() and self._queue.empty()
1691 def SdkToolsPopen(commands, cwd=None, shell=False, output=True):
1693 if cmd not in config:
1694 config[cmd] = find_sdk_tools_cmd(commands[0])
1695 return FDroidPopen([config[cmd]] + commands[1:],
1696 cwd=cwd, shell=shell, output=output)
1699 def FDroidPopen(commands, cwd=None, shell=False, output=True):
1701 Run a command and capture the possibly huge output.
1703 :param commands: command and argument list like in subprocess.Popen
1704 :param cwd: optionally specifies a working directory
1705 :returns: A PopenResult.
1711 cwd = os.path.normpath(cwd)
1712 logging.debug("Directory: %s" % cwd)
1713 logging.debug("> %s" % ' '.join(commands))
1715 result = PopenResult()
1718 p = subprocess.Popen(commands, cwd=cwd, shell=shell, env=env,
1719 stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
1721 raise BuildException("OSError while trying to execute " + ' '.join(commands) + ': ' + str(e))
1723 stdout_queue = Queue.Queue()
1724 stdout_reader = AsynchronousFileReader(p.stdout, stdout_queue)
1725 stdout_reader.start()
1727 # Check the queue for output (until there is no more to get)
1728 while not stdout_reader.eof():
1729 while not stdout_queue.empty():
1730 line = stdout_queue.get()
1731 if output and options.verbose:
1732 # Output directly to console
1733 sys.stderr.write(line)
1735 result.output += line
1739 result.returncode = p.wait()
1743 def remove_signing_keys(build_dir):
1744 comment = re.compile(r'[ ]*//')
1745 signing_configs = re.compile(r'^[\t ]*signingConfigs[ \t]*{[ \t]*$')
1747 re.compile(r'^[\t ]*signingConfig [^ ]*$'),
1748 re.compile(r'.*android\.signingConfigs\.[^{]*$'),
1749 re.compile(r'.*variant\.outputFile = .*'),
1750 re.compile(r'.*output\.outputFile = .*'),
1751 re.compile(r'.*\.readLine\(.*'),
1753 for root, dirs, files in os.walk(build_dir):
1754 if 'build.gradle' in files:
1755 path = os.path.join(root, 'build.gradle')
1757 with open(path, "r") as o:
1758 lines = o.readlines()
1764 with open(path, "w") as o:
1765 while i < len(lines):
1768 while line.endswith('\\\n'):
1769 line = line.rstrip('\\\n') + lines[i]
1772 if comment.match(line):
1776 opened += line.count('{')
1777 opened -= line.count('}')
1780 if signing_configs.match(line):
1785 if any(s.match(line) for s in line_matches):
1793 logging.info("Cleaned build.gradle of keysigning configs at %s" % path)
1796 'project.properties',
1798 'default.properties',
1801 if propfile in files:
1802 path = os.path.join(root, propfile)
1804 with open(path, "r") as o:
1805 lines = o.readlines()
1809 with open(path, "w") as o:
1811 if any(line.startswith(s) for s in ('key.store', 'key.alias')):
1818 logging.info("Cleaned %s of keysigning configs at %s" % (propfile, path))
1821 def reset_env_path():
1822 global env, orig_path
1823 env['PATH'] = orig_path
1826 def add_to_env_path(path):
1828 paths = env['PATH'].split(os.pathsep)
1832 env['PATH'] = os.pathsep.join(paths)
1835 def replace_config_vars(cmd):
1837 cmd = cmd.replace('$$SDK$$', config['sdk_path'])
1838 # env['ANDROID_NDK'] is set in build_local right before prepare_source
1839 cmd = cmd.replace('$$NDK$$', env['ANDROID_NDK'])
1840 cmd = cmd.replace('$$MVN3$$', config['mvn3'])
1844 def place_srclib(root_dir, number, libpath):
1847 relpath = os.path.relpath(libpath, root_dir)
1848 proppath = os.path.join(root_dir, 'project.properties')
1851 if os.path.isfile(proppath):
1852 with open(proppath, "r") as o:
1853 lines = o.readlines()
1855 with open(proppath, "w") as o:
1858 if line.startswith('android.library.reference.%d=' % number):
1859 o.write('android.library.reference.%d=%s\n' % (number, relpath))
1864 o.write('android.library.reference.%d=%s\n' % (number, relpath))
1867 def compare_apks(apk1, apk2, tmp_dir):
1870 Returns None if the apk content is the same (apart from the signing key),
1871 otherwise a string describing what's different, or what went wrong when
1872 trying to do the comparison.
1875 thisdir = os.path.join(tmp_dir, 'this_apk')
1876 thatdir = os.path.join(tmp_dir, 'that_apk')
1877 for d in [thisdir, thatdir]:
1878 if os.path.exists(d):
1882 if subprocess.call(['jar', 'xf',
1883 os.path.abspath(apk1)],
1885 return("Failed to unpack " + apk1)
1886 if subprocess.call(['jar', 'xf',
1887 os.path.abspath(apk2)],
1889 return("Failed to unpack " + apk2)
1891 p = FDroidPopen(['diff', '-r', 'this_apk', 'that_apk'], cwd=tmp_dir,
1893 lines = p.output.splitlines()
1894 if len(lines) != 1 or 'META-INF' not in lines[0]:
1895 return("Unexpected diff output - " + p.output)
1897 # If we get here, it seems like they're the same!