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 if self.repotype == 'git-svn':
431 raise VCSException("Authentication is not supported for git-svn")
432 self.username, remote = remote.split('@')
433 if ':' not in self.username:
434 raise VCSException("Password required with username")
435 self.username, self.password = self.username.split(':')
439 self.clone_failed = False
440 self.refreshed = False
446 # Take the local repository to a clean version of the given revision, which
447 # is specificed in the VCS's native format. Beforehand, the repository can
448 # be dirty, or even non-existent. If the repository does already exist
449 # locally, it will be updated from the origin, but only once in the
450 # lifetime of the vcs object.
451 # None is acceptable for 'rev' if you know you are cloning a clean copy of
452 # the repo - otherwise it must specify a valid revision.
453 def gotorevision(self, rev):
455 if self.clone_failed:
456 raise VCSException("Downloading the repository already failed once, not trying again.")
458 # The .fdroidvcs-id file for a repo tells us what VCS type
459 # and remote that directory was created from, allowing us to drop it
460 # automatically if either of those things changes.
461 fdpath = os.path.join(self.local, '..',
462 '.fdroidvcs-' + os.path.basename(self.local))
463 cdata = self.repotype() + ' ' + self.remote
466 if os.path.exists(self.local):
467 if os.path.exists(fdpath):
468 with open(fdpath, 'r') as f:
469 fsdata = f.read().strip()
474 logging.info("Repository details for %s changed - deleting" % (
478 logging.info("Repository details for %s missing - deleting" % (
481 shutil.rmtree(self.local)
486 self.gotorevisionx(rev)
487 except FDroidException, e:
490 # If necessary, write the .fdroidvcs file.
491 if writeback and not self.clone_failed:
492 with open(fdpath, 'w') as f:
498 # Derived classes need to implement this. It's called once basic checking
499 # has been performend.
500 def gotorevisionx(self, rev):
501 raise VCSException("This VCS type doesn't define gotorevisionx")
503 # Initialise and update submodules
504 def initsubmodules(self):
505 raise VCSException('Submodules not supported for this vcs type')
507 # Get a list of all known tags
509 if not self._gettags:
510 raise VCSException('gettags not supported for this vcs type')
512 for tag in self._gettags():
513 if re.match('[-A-Za-z0-9_. ]+$', tag):
517 def latesttags(self, tags, number):
518 """Get the most recent tags in a given list.
520 :param tags: a list of tags
521 :param number: the number to return
522 :returns: A list containing the most recent tags in the provided
523 list, up to the maximum number given.
525 raise VCSException('latesttags not supported for this vcs type')
527 # Get current commit reference (hash, revision, etc)
529 raise VCSException('getref not supported for this vcs type')
531 # Returns the srclib (name, path) used in setting up the current
542 # If the local directory exists, but is somehow not a git repository, git
543 # will traverse up the directory tree until it finds one that is (i.e.
544 # fdroidserver) and then we'll proceed to destroy it! This is called as
547 p = FDroidPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local, output=False)
548 result = p.output.rstrip()
549 if not result.endswith(self.local):
550 raise VCSException('Repository mismatch')
552 def gotorevisionx(self, rev):
553 if not os.path.exists(self.local):
555 p = FDroidPopen(['git', 'clone', self.remote, self.local])
556 if p.returncode != 0:
557 self.clone_failed = True
558 raise VCSException("Git clone failed", p.output)
562 # Discard any working tree changes
563 p = FDroidPopen(['git', 'reset', '--hard'], cwd=self.local, output=False)
564 if p.returncode != 0:
565 raise VCSException("Git reset failed", p.output)
566 # Remove untracked files now, in case they're tracked in the target
567 # revision (it happens!)
568 p = FDroidPopen(['git', 'clean', '-dffx'], cwd=self.local, output=False)
569 if p.returncode != 0:
570 raise VCSException("Git clean failed", p.output)
571 if not self.refreshed:
572 # Get latest commits and tags from remote
573 p = FDroidPopen(['git', 'fetch', 'origin'], cwd=self.local)
574 if p.returncode != 0:
575 raise VCSException("Git fetch failed", p.output)
576 p = FDroidPopen(['git', 'fetch', '--prune', '--tags', 'origin'], cwd=self.local, output=False)
577 if p.returncode != 0:
578 raise VCSException("Git fetch failed", p.output)
579 # Recreate origin/HEAD as git clone would do it, in case it disappeared
580 p = FDroidPopen(['git', 'remote', 'set-head', 'origin', '--auto'], cwd=self.local, output=False)
581 if p.returncode != 0:
582 lines = p.output.splitlines()
583 if 'Multiple remote HEAD branches' not in lines[0]:
584 raise VCSException("Git remote set-head failed", p.output)
585 branch = lines[1].split(' ')[-1]
586 p2 = FDroidPopen(['git', 'remote', 'set-head', 'origin', branch], cwd=self.local, output=False)
587 if p2.returncode != 0:
588 raise VCSException("Git remote set-head failed", p.output + '\n' + p2.output)
589 self.refreshed = True
590 # origin/HEAD is the HEAD of the remote, e.g. the "default branch" on
591 # a github repo. Most of the time this is the same as origin/master.
592 rev = rev or 'origin/HEAD'
593 p = FDroidPopen(['git', 'checkout', '-f', rev], cwd=self.local, output=False)
594 if p.returncode != 0:
595 raise VCSException("Git checkout of '%s' failed" % rev, p.output)
596 # Get rid of any uncontrolled files left behind
597 p = FDroidPopen(['git', 'clean', '-dffx'], cwd=self.local, output=False)
598 if p.returncode != 0:
599 raise VCSException("Git clean failed", p.output)
601 def initsubmodules(self):
603 submfile = os.path.join(self.local, '.gitmodules')
604 if not os.path.isfile(submfile):
605 raise VCSException("No git submodules available")
607 # fix submodules not accessible without an account and public key auth
608 with open(submfile, 'r') as f:
609 lines = f.readlines()
610 with open(submfile, 'w') as f:
612 if 'git@github.com' in line:
613 line = line.replace('git@github.com:', 'https://github.com/')
617 ['git', 'reset', '--hard'],
618 ['git', 'clean', '-dffx'],
620 p = FDroidPopen(['git', 'submodule', 'foreach', '--recursive'] + cmd, cwd=self.local, output=False)
621 if p.returncode != 0:
622 raise VCSException("Git submodule reset failed", p.output)
623 p = FDroidPopen(['git', 'submodule', 'sync'], cwd=self.local, output=False)
624 if p.returncode != 0:
625 raise VCSException("Git submodule sync failed", p.output)
626 p = FDroidPopen(['git', 'submodule', 'update', '--init', '--force', '--recursive'], cwd=self.local)
627 if p.returncode != 0:
628 raise VCSException("Git submodule update failed", p.output)
632 p = FDroidPopen(['git', 'tag'], cwd=self.local, output=False)
633 return p.output.splitlines()
635 def latesttags(self, tags, number):
640 ['git', 'show', '--format=format:%ct', '-s', tag],
641 cwd=self.local, output=False)
642 # Timestamp is on the last line. For a normal tag, it's the only
643 # line, but for annotated tags, the rest of the info precedes it.
644 ts = int(p.output.splitlines()[-1])
647 for _, t in sorted(tl)[-number:]:
652 class vcs_gitsvn(vcs):
657 # If the local directory exists, but is somehow not a git repository, git
658 # will traverse up the directory tree until it finds one that is (i.e.
659 # fdroidserver) and then we'll proceed to destory it! This is called as
662 p = FDroidPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local, output=False)
663 result = p.output.rstrip()
664 if not result.endswith(self.local):
665 raise VCSException('Repository mismatch')
667 def gotorevisionx(self, rev):
668 if not os.path.exists(self.local):
670 gitsvn_args = ['git', 'svn', 'clone']
671 if ';' in self.remote:
672 remote_split = self.remote.split(';')
673 for i in remote_split[1:]:
674 if i.startswith('trunk='):
675 gitsvn_args.extend(['-T', i[6:]])
676 elif i.startswith('tags='):
677 gitsvn_args.extend(['-t', i[5:]])
678 elif i.startswith('branches='):
679 gitsvn_args.extend(['-b', i[9:]])
680 gitsvn_args.extend([remote_split[0], self.local])
681 p = FDroidPopen(gitsvn_args, output=False)
682 if p.returncode != 0:
683 self.clone_failed = True
684 raise VCSException("Git svn clone failed", p.output)
686 gitsvn_args.extend([self.remote, self.local])
687 p = FDroidPopen(gitsvn_args, output=False)
688 if p.returncode != 0:
689 self.clone_failed = True
690 raise VCSException("Git svn clone failed", p.output)
694 # Discard any working tree changes
695 p = FDroidPopen(['git', 'reset', '--hard'], cwd=self.local, output=False)
696 if p.returncode != 0:
697 raise VCSException("Git reset failed", p.output)
698 # Remove untracked files now, in case they're tracked in the target
699 # revision (it happens!)
700 p = FDroidPopen(['git', 'clean', '-dffx'], cwd=self.local, output=False)
701 if p.returncode != 0:
702 raise VCSException("Git clean failed", p.output)
703 if not self.refreshed:
704 # Get new commits, branches and tags from repo
705 p = FDroidPopen(['git', 'svn', 'fetch'], cwd=self.local, output=False)
706 if p.returncode != 0:
707 raise VCSException("Git svn fetch failed")
708 p = FDroidPopen(['git', 'svn', 'rebase'], cwd=self.local, output=False)
709 if p.returncode != 0:
710 raise VCSException("Git svn rebase failed", p.output)
711 self.refreshed = True
713 rev = rev or 'master'
715 nospaces_rev = rev.replace(' ', '%20')
716 # Try finding a svn tag
717 for treeish in ['origin/', '']:
718 p = FDroidPopen(['git', 'checkout', treeish + 'tags/' + nospaces_rev], cwd=self.local, output=False)
719 if p.returncode == 0:
721 if p.returncode != 0:
722 # No tag found, normal svn rev translation
723 # Translate svn rev into git format
724 rev_split = rev.split('/')
727 for treeish in ['origin/', '']:
728 if len(rev_split) > 1:
729 treeish += rev_split[0]
730 svn_rev = rev_split[1]
733 # if no branch is specified, then assume trunk (i.e. 'master' branch):
737 svn_rev = svn_rev if svn_rev[0] == 'r' else 'r' + svn_rev
739 p = FDroidPopen(['git', 'svn', 'find-rev', '--before', svn_rev, treeish], cwd=self.local, output=False)
740 git_rev = p.output.rstrip()
742 if p.returncode == 0 and git_rev:
745 if p.returncode != 0 or not git_rev:
746 # Try a plain git checkout as a last resort
747 p = FDroidPopen(['git', 'checkout', rev], cwd=self.local, output=False)
748 if p.returncode != 0:
749 raise VCSException("No git treeish found and direct git checkout of '%s' failed" % rev, p.output)
751 # Check out the git rev equivalent to the svn rev
752 p = FDroidPopen(['git', 'checkout', git_rev], cwd=self.local, output=False)
753 if p.returncode != 0:
754 raise VCSException("Git checkout of '%s' failed" % rev, p.output)
756 # Get rid of any uncontrolled files left behind
757 p = FDroidPopen(['git', 'clean', '-dffx'], cwd=self.local, output=False)
758 if p.returncode != 0:
759 raise VCSException("Git clean failed", p.output)
763 for treeish in ['origin/', '']:
764 d = os.path.join(self.local, '.git', 'svn', 'refs', 'remotes', treeish, 'tags')
770 p = FDroidPopen(['git', 'svn', 'find-rev', 'HEAD'], cwd=self.local, output=False)
771 if p.returncode != 0:
773 return p.output.strip()
781 def gotorevisionx(self, rev):
782 if not os.path.exists(self.local):
783 p = FDroidPopen(['hg', 'clone', self.remote, self.local], output=False)
784 if p.returncode != 0:
785 self.clone_failed = True
786 raise VCSException("Hg clone failed", p.output)
788 p = FDroidPopen(['hg', 'status', '-uS'], cwd=self.local, output=False)
789 if p.returncode != 0:
790 raise VCSException("Hg status failed", p.output)
791 for line in p.output.splitlines():
792 if not line.startswith('? '):
793 raise VCSException("Unexpected output from hg status -uS: " + line)
794 FDroidPopen(['rm', '-rf', line[2:]], cwd=self.local, output=False)
795 if not self.refreshed:
796 p = FDroidPopen(['hg', 'pull'], cwd=self.local, output=False)
797 if p.returncode != 0:
798 raise VCSException("Hg pull failed", p.output)
799 self.refreshed = True
801 rev = rev or 'default'
804 p = FDroidPopen(['hg', 'update', '-C', rev], cwd=self.local, output=False)
805 if p.returncode != 0:
806 raise VCSException("Hg checkout of '%s' failed" % rev, p.output)
807 p = FDroidPopen(['hg', 'purge', '--all'], cwd=self.local, output=False)
808 # Also delete untracked files, we have to enable purge extension for that:
809 if "'purge' is provided by the following extension" in p.output:
810 with open(os.path.join(self.local, '.hg', 'hgrc'), "a") as myfile:
811 myfile.write("\n[extensions]\nhgext.purge=\n")
812 p = FDroidPopen(['hg', 'purge', '--all'], cwd=self.local, output=False)
813 if p.returncode != 0:
814 raise VCSException("HG purge failed", p.output)
815 elif p.returncode != 0:
816 raise VCSException("HG purge failed", p.output)
819 p = FDroidPopen(['hg', 'tags', '-q'], cwd=self.local, output=False)
820 return p.output.splitlines()[1:]
828 def gotorevisionx(self, rev):
829 if not os.path.exists(self.local):
830 p = FDroidPopen(['bzr', 'branch', self.remote, self.local], output=False)
831 if p.returncode != 0:
832 self.clone_failed = True
833 raise VCSException("Bzr branch failed", p.output)
835 p = FDroidPopen(['bzr', 'clean-tree', '--force', '--unknown', '--ignored'], cwd=self.local, output=False)
836 if p.returncode != 0:
837 raise VCSException("Bzr revert failed", p.output)
838 if not self.refreshed:
839 p = FDroidPopen(['bzr', 'pull'], cwd=self.local, output=False)
840 if p.returncode != 0:
841 raise VCSException("Bzr update failed", p.output)
842 self.refreshed = True
844 revargs = list(['-r', rev] if rev else [])
845 p = FDroidPopen(['bzr', 'revert'] + revargs, cwd=self.local, output=False)
846 if p.returncode != 0:
847 raise VCSException("Bzr revert of '%s' failed" % rev, p.output)
850 p = FDroidPopen(['bzr', 'tags'], cwd=self.local, output=False)
851 return [tag.split(' ')[0].strip() for tag in
852 p.output.splitlines()]
855 def retrieve_string(app_dir, string, xmlfiles=None):
858 os.path.join(app_dir, 'res'),
859 os.path.join(app_dir, 'src', 'main'),
864 for res_dir in res_dirs:
865 for r, d, f in os.walk(res_dir):
866 if os.path.basename(r) == 'values':
867 xmlfiles += [os.path.join(r, x) for x in f if x.endswith('.xml')]
870 if string.startswith('@string/'):
871 string_search = re.compile(r'.*name="' + string[8:] + '".*?>"?([^<]+?)"?<.*').search
872 elif string.startswith('&') and string.endswith(';'):
873 string_search = re.compile(r'.*<!ENTITY.*' + string[1:-1] + '.*?"([^"]+?)".*>').search
875 if string_search is not None:
876 for xmlfile in xmlfiles:
877 for line in file(xmlfile):
878 matches = string_search(line)
880 return retrieve_string(app_dir, matches.group(1), xmlfiles)
883 return string.replace("\\'", "'")
886 # Return list of existing files that will be used to find the highest vercode
887 def manifest_paths(app_dir, flavours):
889 possible_manifests = \
890 [os.path.join(app_dir, 'AndroidManifest.xml'),
891 os.path.join(app_dir, 'src', 'main', 'AndroidManifest.xml'),
892 os.path.join(app_dir, 'src', 'AndroidManifest.xml'),
893 os.path.join(app_dir, 'build.gradle')]
895 for flavour in flavours:
898 possible_manifests.append(
899 os.path.join(app_dir, 'src', flavour, 'AndroidManifest.xml'))
901 return [path for path in possible_manifests if os.path.isfile(path)]
904 # Retrieve the package name. Returns the name, or None if not found.
905 def fetch_real_name(app_dir, flavours):
906 app_search = re.compile(r'.*<application.*').search
907 name_search = re.compile(r'.*android:label="([^"]+)".*').search
909 for f in manifest_paths(app_dir, flavours):
910 if not has_extension(f, 'xml'):
912 logging.debug("fetch_real_name: Checking manifest at " + f)
918 matches = name_search(line)
920 stringname = matches.group(1)
921 logging.debug("fetch_real_name: using string " + stringname)
922 result = retrieve_string(app_dir, stringname)
924 result = result.strip()
929 # Retrieve the version name
930 def version_name(original, app_dir, flavours):
931 for f in manifest_paths(app_dir, flavours):
932 if not has_extension(f, 'xml'):
934 string = retrieve_string(app_dir, original)
940 def get_library_references(root_dir):
942 proppath = os.path.join(root_dir, 'project.properties')
943 if not os.path.isfile(proppath):
945 with open(proppath) as f:
946 for line in f.readlines():
947 if not line.startswith('android.library.reference.'):
949 path = line.split('=')[1].strip()
950 relpath = os.path.join(root_dir, path)
951 if not os.path.isdir(relpath):
953 logging.debug("Found subproject at %s" % path)
954 libraries.append(path)
958 def ant_subprojects(root_dir):
959 subprojects = get_library_references(root_dir)
960 for subpath in subprojects:
961 subrelpath = os.path.join(root_dir, subpath)
962 for p in get_library_references(subrelpath):
963 relp = os.path.normpath(os.path.join(subpath, p))
964 if relp not in subprojects:
965 subprojects.insert(0, relp)
969 def remove_debuggable_flags(root_dir):
970 # Remove forced debuggable flags
971 logging.debug("Removing debuggable flags from %s" % root_dir)
972 for root, dirs, files in os.walk(root_dir):
973 if 'AndroidManifest.xml' in files:
974 path = os.path.join(root, 'AndroidManifest.xml')
975 p = FDroidPopen(['sed', '-i', 's/android:debuggable="[^"]*"//g', path], output=False)
976 if p.returncode != 0:
977 raise BuildException("Failed to remove debuggable flags of %s" % path)
980 # Extract some information from the AndroidManifest.xml at the given path.
981 # Returns (version, vercode, package), any or all of which might be None.
982 # All values returned are strings.
983 def parse_androidmanifests(paths, ignoreversions=None):
986 return (None, None, None)
988 vcsearch = re.compile(r'.*:versionCode="([0-9]+?)".*').search
989 vnsearch = re.compile(r'.*:versionName="([^"]+?)".*').search
990 psearch = re.compile(r'.*package="([^"]+)".*').search
992 vcsearch_g = re.compile(r'.*versionCode *=* *["\']*([0-9]+)["\']*').search
993 vnsearch_g = re.compile(r'.*versionName *=* *(["\'])((?:(?=(\\?))\3.)*?)\1.*').search
994 psearch_g = re.compile(r'.*packageName *=* *["\']([^"]+)["\'].*').search
996 ignoresearch = re.compile(ignoreversions).search if ignoreversions else None
1004 logging.debug("Parsing manifest at {0}".format(path))
1005 gradle = has_extension(path, 'gradle')
1008 # Remember package name, may be defined separately from version+vercode
1009 package = max_package
1011 for line in file(path):
1014 matches = psearch_g(line)
1016 matches = psearch(line)
1018 package = matches.group(1)
1021 matches = vnsearch_g(line)
1023 matches = vnsearch(line)
1025 version = matches.group(2 if gradle else 1)
1028 matches = vcsearch_g(line)
1030 matches = vcsearch(line)
1032 vercode = matches.group(1)
1034 logging.debug("..got package={0}, version={1}, vercode={2}"
1035 .format(package, version, vercode))
1037 # Always grab the package name and version name in case they are not
1038 # together with the highest version code
1039 if max_package is None and package is not None:
1040 max_package = package
1041 if max_version is None and version is not None:
1042 max_version = version
1044 if max_vercode is None or (vercode is not None and vercode > max_vercode):
1045 if not ignoresearch or not ignoresearch(version):
1046 if version is not None:
1047 max_version = version
1048 if vercode is not None:
1049 max_vercode = vercode
1050 if package is not None:
1051 max_package = package
1053 max_version = "Ignore"
1055 if max_version is None:
1056 max_version = "Unknown"
1058 if not is_valid_package_name(max_package):
1059 raise FDroidException("Invalid package name {0}".format(max_package))
1061 return (max_version, max_vercode, max_package)
1064 def is_valid_package_name(name):
1065 return re.match("[A-Za-z_][A-Za-z_0-9.]+$", name)
1068 class FDroidException(Exception):
1070 def __init__(self, value, detail=None):
1072 self.detail = detail
1074 def get_wikitext(self):
1075 ret = repr(self.value) + "\n"
1079 txt = self.detail[-8192:] if len(self.detail) > 8192 else self.detail
1087 ret += "\n==== detail begin ====\n%s\n==== detail end ====" % self.detail.strip()
1091 class VCSException(FDroidException):
1095 class BuildException(FDroidException):
1099 # Get the specified source library.
1100 # Returns the path to it. Normally this is the path to be used when referencing
1101 # it, which may be a subdirectory of the actual project. If you want the base
1102 # directory of the project, pass 'basepath=True'.
1103 def getsrclib(spec, srclib_dir, srclibpaths=[], subdir=None,
1104 basepath=False, raw=False, prepare=True, preponly=False):
1112 name, ref = spec.split('@')
1114 number, name = name.split(':', 1)
1116 name, subdir = name.split('/', 1)
1118 if name not in metadata.srclibs:
1119 raise VCSException('srclib ' + name + ' not found.')
1121 srclib = metadata.srclibs[name]
1123 sdir = os.path.join(srclib_dir, name)
1126 vcs = getvcs(srclib["Repo Type"], srclib["Repo"], sdir)
1127 vcs.srclib = (name, number, sdir)
1129 vcs.gotorevision(ref)
1136 libdir = os.path.join(sdir, subdir)
1137 elif srclib["Subdir"]:
1138 for subdir in srclib["Subdir"]:
1139 libdir_candidate = os.path.join(sdir, subdir)
1140 if os.path.exists(libdir_candidate):
1141 libdir = libdir_candidate
1147 if srclib["Srclibs"]:
1149 for lib in srclib["Srclibs"].replace(';', ',').split(','):
1151 for t in srclibpaths:
1156 raise VCSException('Missing recursive srclib %s for %s' % (
1158 place_srclib(libdir, n, s_tuple[2])
1161 remove_signing_keys(sdir)
1162 remove_debuggable_flags(sdir)
1166 if srclib["Prepare"]:
1167 cmd = replace_config_vars(srclib["Prepare"])
1169 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=libdir)
1170 if p.returncode != 0:
1171 raise BuildException("Error running prepare command for srclib %s"
1177 return (name, number, libdir)
1180 # Prepare the source code for a particular build
1181 # 'vcs' - the appropriate vcs object for the application
1182 # 'app' - the application details from the metadata
1183 # 'build' - the build details from the metadata
1184 # 'build_dir' - the path to the build directory, usually
1186 # 'srclib_dir' - the path to the source libraries directory, usually
1188 # 'extlib_dir' - the path to the external libraries directory, usually
1190 # Returns the (root, srclibpaths) where:
1191 # 'root' is the root directory, which may be the same as 'build_dir' or may
1192 # be a subdirectory of it.
1193 # 'srclibpaths' is information on the srclibs being used
1194 def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=False):
1196 # Optionally, the actual app source can be in a subdirectory
1198 root_dir = os.path.join(build_dir, build['subdir'])
1200 root_dir = build_dir
1202 # Get a working copy of the right revision
1203 logging.info("Getting source for revision " + build['commit'])
1204 vcs.gotorevision(build['commit'])
1206 # Initialise submodules if requred
1207 if build['submodules']:
1208 logging.info("Initialising submodules")
1209 vcs.initsubmodules()
1211 # Check that a subdir (if we're using one) exists. This has to happen
1212 # after the checkout, since it might not exist elsewhere
1213 if not os.path.exists(root_dir):
1214 raise BuildException('Missing subdir ' + root_dir)
1216 # Run an init command if one is required
1218 cmd = replace_config_vars(build['init'])
1219 logging.info("Running 'init' commands in %s" % root_dir)
1221 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
1222 if p.returncode != 0:
1223 raise BuildException("Error running init command for %s:%s" %
1224 (app['id'], build['version']), p.output)
1226 # Apply patches if any
1228 logging.info("Applying patches")
1229 for patch in build['patch']:
1230 patch = patch.strip()
1231 logging.info("Applying " + patch)
1232 patch_path = os.path.join('metadata', app['id'], patch)
1233 p = FDroidPopen(['patch', '-p1', '-i', os.path.abspath(patch_path)], cwd=build_dir)
1234 if p.returncode != 0:
1235 raise BuildException("Failed to apply patch %s" % patch_path)
1237 # Get required source libraries
1239 if build['srclibs']:
1240 logging.info("Collecting source libraries")
1241 for lib in build['srclibs']:
1242 srclibpaths.append(getsrclib(lib, srclib_dir, build, srclibpaths,
1245 for name, number, libpath in srclibpaths:
1246 place_srclib(root_dir, int(number) if number else None, libpath)
1248 basesrclib = vcs.getsrclib()
1249 # If one was used for the main source, add that too.
1251 srclibpaths.append(basesrclib)
1253 # Update the local.properties file
1254 localprops = [os.path.join(build_dir, 'local.properties')]
1256 localprops += [os.path.join(root_dir, 'local.properties')]
1257 for path in localprops:
1259 if os.path.isfile(path):
1260 logging.info("Updating local.properties file at %s" % path)
1266 logging.info("Creating local.properties file at %s" % path)
1267 # Fix old-fashioned 'sdk-location' by copying
1268 # from sdk.dir, if necessary
1269 if build['oldsdkloc']:
1270 sdkloc = re.match(r".*^sdk.dir=(\S+)$.*", props,
1271 re.S | re.M).group(1)
1272 props += "sdk-location=%s\n" % sdkloc
1274 props += "sdk.dir=%s\n" % config['sdk_path']
1275 props += "sdk-location=%s\n" % config['sdk_path']
1276 if build['ndk_path']:
1278 props += "ndk.dir=%s\n" % build['ndk_path']
1279 props += "ndk-location=%s\n" % build['ndk_path']
1280 # Add java.encoding if necessary
1281 if build['encoding']:
1282 props += "java.encoding=%s\n" % build['encoding']
1288 if build['type'] == 'gradle':
1289 flavours = build['gradle']
1291 version_regex = re.compile(r".*'com\.android\.tools\.build:gradle:([^\.]+\.[^\.]+).*'.*")
1292 gradlepluginver = None
1294 gradle_files = [os.path.join(root_dir, 'build.gradle')]
1296 # Parent dir build.gradle
1297 parent_dir = os.path.normpath(os.path.join(root_dir, '..'))
1298 if parent_dir.startswith(build_dir):
1299 gradle_files.append(os.path.join(parent_dir, 'build.gradle'))
1301 for path in gradle_files:
1304 if not os.path.isfile(path):
1306 with open(path) as f:
1308 match = version_regex.match(line)
1310 gradlepluginver = match.group(1)
1314 build['gradlepluginver'] = LooseVersion(gradlepluginver)
1316 logging.warn("Could not fetch the gradle plugin version, defaulting to 0.11")
1317 build['gradlepluginver'] = LooseVersion('0.11')
1320 n = build["target"].split('-')[1]
1321 FDroidPopen(['sed', '-i',
1322 's@compileSdkVersion *[0-9]*@compileSdkVersion ' + n + '@g',
1323 'build.gradle'], cwd=root_dir, output=False)
1325 # Remove forced debuggable flags
1326 remove_debuggable_flags(root_dir)
1328 # Insert version code and number into the manifest if necessary
1329 if build['forceversion']:
1330 logging.info("Changing the version name")
1331 for path in manifest_paths(root_dir, flavours):
1332 if not os.path.isfile(path):
1334 if has_extension(path, 'xml'):
1335 p = FDroidPopen(['sed', '-i',
1336 's/android:versionName="[^"]*"/android:versionName="' + build['version'] + '"/g',
1337 path], output=False)
1338 if p.returncode != 0:
1339 raise BuildException("Failed to amend manifest")
1340 elif has_extension(path, 'gradle'):
1341 p = FDroidPopen(['sed', '-i',
1342 's/versionName *=* *.*/versionName = "' + build['version'] + '"/g',
1343 path], output=False)
1344 if p.returncode != 0:
1345 raise BuildException("Failed to amend build.gradle")
1346 if build['forcevercode']:
1347 logging.info("Changing the version code")
1348 for path in manifest_paths(root_dir, flavours):
1349 if not os.path.isfile(path):
1351 if has_extension(path, 'xml'):
1352 p = FDroidPopen(['sed', '-i',
1353 's/android:versionCode="[^"]*"/android:versionCode="' + build['vercode'] + '"/g',
1354 path], output=False)
1355 if p.returncode != 0:
1356 raise BuildException("Failed to amend manifest")
1357 elif has_extension(path, 'gradle'):
1358 p = FDroidPopen(['sed', '-i',
1359 's/versionCode *=* *[0-9]*/versionCode = ' + build['vercode'] + '/g',
1360 path], output=False)
1361 if p.returncode != 0:
1362 raise BuildException("Failed to amend build.gradle")
1364 # Delete unwanted files
1366 logging.info("Removing specified files")
1367 for part in getpaths(build_dir, build, 'rm'):
1368 dest = os.path.join(build_dir, part)
1369 logging.info("Removing {0}".format(part))
1370 if os.path.lexists(dest):
1371 if os.path.islink(dest):
1372 FDroidPopen(['unlink', dest], output=False)
1374 FDroidPopen(['rm', '-rf', dest], output=False)
1376 logging.info("...but it didn't exist")
1378 remove_signing_keys(build_dir)
1380 # Add required external libraries
1381 if build['extlibs']:
1382 logging.info("Collecting prebuilt libraries")
1383 libsdir = os.path.join(root_dir, 'libs')
1384 if not os.path.exists(libsdir):
1386 for lib in build['extlibs']:
1388 logging.info("...installing extlib {0}".format(lib))
1389 libf = os.path.basename(lib)
1390 libsrc = os.path.join(extlib_dir, lib)
1391 if not os.path.exists(libsrc):
1392 raise BuildException("Missing extlib file {0}".format(libsrc))
1393 shutil.copyfile(libsrc, os.path.join(libsdir, libf))
1395 # Run a pre-build command if one is required
1396 if build['prebuild']:
1397 logging.info("Running 'prebuild' commands in %s" % root_dir)
1399 cmd = replace_config_vars(build['prebuild'])
1401 # Substitute source library paths into prebuild commands
1402 for name, number, libpath in srclibpaths:
1403 libpath = os.path.relpath(libpath, root_dir)
1404 cmd = cmd.replace('$$' + name + '$$', libpath)
1406 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
1407 if p.returncode != 0:
1408 raise BuildException("Error running prebuild command for %s:%s" %
1409 (app['id'], build['version']), p.output)
1411 # Generate (or update) the ant build file, build.xml...
1412 if build['update'] and build['update'] != ['no'] and build['type'] == 'ant':
1413 parms = ['android', 'update', 'lib-project']
1414 lparms = ['android', 'update', 'project']
1417 parms += ['-t', build['target']]
1418 lparms += ['-t', build['target']]
1419 if build['update'] == ['auto']:
1420 update_dirs = ant_subprojects(root_dir) + ['.']
1422 update_dirs = build['update']
1424 for d in update_dirs:
1425 subdir = os.path.join(root_dir, d)
1427 logging.debug("Updating main project")
1428 cmd = parms + ['-p', d]
1430 logging.debug("Updating subproject %s" % d)
1431 cmd = lparms + ['-p', d]
1432 p = SdkToolsPopen(cmd, cwd=root_dir)
1433 # Check to see whether an error was returned without a proper exit
1434 # code (this is the case for the 'no target set or target invalid'
1436 if p.returncode != 0 or p.output.startswith("Error: "):
1437 raise BuildException("Failed to update project at %s" % d, p.output)
1438 # Clean update dirs via ant
1440 logging.info("Cleaning subproject %s" % d)
1441 p = FDroidPopen(['ant', 'clean'], cwd=subdir)
1443 return (root_dir, srclibpaths)
1446 # Split and extend via globbing the paths from a field
1447 def getpaths(build_dir, build, field):
1449 for p in build[field]:
1451 full_path = os.path.join(build_dir, p)
1452 full_path = os.path.normpath(full_path)
1453 paths += [r[len(build_dir) + 1:] for r in glob.glob(full_path)]
1457 # Scan the source code in the given directory (and all subdirectories)
1458 # and return the number of fatal problems encountered
1459 def scan_source(build_dir, root_dir, thisbuild):
1463 # Common known non-free blobs (always lower case):
1465 re.compile(r'flurryagent', re.IGNORECASE),
1466 re.compile(r'paypal.*mpl', re.IGNORECASE),
1467 re.compile(r'google.*analytics', re.IGNORECASE),
1468 re.compile(r'admob.*sdk.*android', re.IGNORECASE),
1469 re.compile(r'google.*ad.*view', re.IGNORECASE),
1470 re.compile(r'google.*admob', re.IGNORECASE),
1471 re.compile(r'google.*play.*services', re.IGNORECASE),
1472 re.compile(r'crittercism', re.IGNORECASE),
1473 re.compile(r'heyzap', re.IGNORECASE),
1474 re.compile(r'jpct.*ae', re.IGNORECASE),
1475 re.compile(r'youtube.*android.*player.*api', re.IGNORECASE),
1476 re.compile(r'bugsense', re.IGNORECASE),
1477 re.compile(r'crashlytics', re.IGNORECASE),
1478 re.compile(r'ouya.*sdk', re.IGNORECASE),
1479 re.compile(r'libspen23', re.IGNORECASE),
1482 scanignore = getpaths(build_dir, thisbuild, 'scanignore')
1483 scandelete = getpaths(build_dir, thisbuild, 'scandelete')
1485 scanignore_worked = set()
1486 scandelete_worked = set()
1489 ms = magic.open(magic.MIME_TYPE)
1491 except AttributeError:
1495 for p in scanignore:
1496 if fd.startswith(p):
1497 scanignore_worked.add(p)
1502 for p in scandelete:
1503 if fd.startswith(p):
1504 scandelete_worked.add(p)
1508 def ignoreproblem(what, fd, fp):
1509 logging.info('Ignoring %s at %s' % (what, fd))
1512 def removeproblem(what, fd, fp):
1513 logging.info('Removing %s at %s' % (what, fd))
1517 def warnproblem(what, fd):
1518 logging.warn('Found %s at %s' % (what, fd))
1520 def handleproblem(what, fd, fp):
1522 return ignoreproblem(what, fd, fp)
1524 return removeproblem(what, fd, fp)
1525 logging.error('Found %s at %s' % (what, fd))
1528 # Iterate through all files in the source code
1529 for r, d, f in os.walk(build_dir, topdown=True):
1531 # It's topdown, so checking the basename is enough
1532 for ignoredir in ('.hg', '.git', '.svn', '.bzr'):
1538 # Path (relative) to the file
1539 fp = os.path.join(r, curfile)
1540 fd = fp[len(build_dir) + 1:]
1543 mime = magic.from_file(fp, mime=True) if ms is None else ms.file(fp)
1544 except UnicodeError:
1545 warnproblem('malformed magic number', fd)
1547 if mime == 'application/x-sharedlib':
1548 count += handleproblem('shared library', fd, fp)
1550 elif mime == 'application/x-archive':
1551 count += handleproblem('static library', fd, fp)
1553 elif mime == 'application/x-executable':
1554 count += handleproblem('binary executable', fd, fp)
1556 elif mime == 'application/x-java-applet':
1557 count += handleproblem('Java compiled class', fd, fp)
1562 'application/java-archive',
1563 'application/octet-stream',
1566 if has_extension(fp, 'apk'):
1567 removeproblem('APK file', fd, fp)
1569 elif has_extension(fp, 'jar'):
1571 if any(suspect.match(curfile) for suspect in usual_suspects):
1572 count += handleproblem('usual supect', fd, fp)
1574 warnproblem('JAR file', fd)
1576 elif has_extension(fp, 'zip'):
1577 warnproblem('ZIP file', fd)
1580 warnproblem('unknown compressed or binary file', fd)
1582 elif has_extension(fp, 'java'):
1583 for line in file(fp):
1584 if 'DexClassLoader' in line:
1585 count += handleproblem('DexClassLoader', fd, fp)
1590 for p in scanignore:
1591 if p not in scanignore_worked:
1592 logging.error('Unused scanignore path: %s' % p)
1595 for p in scandelete:
1596 if p not in scandelete_worked:
1597 logging.error('Unused scandelete path: %s' % p)
1600 # Presence of a jni directory without buildjni=yes might
1601 # indicate a problem (if it's not a problem, explicitly use
1602 # buildjni=no to bypass this check)
1603 if (os.path.exists(os.path.join(root_dir, 'jni')) and
1604 not thisbuild['buildjni']):
1605 logging.error('Found jni directory, but buildjni is not enabled. Set it to \'no\' to ignore.')
1614 self.path = os.path.join('stats', 'known_apks.txt')
1616 if os.path.exists(self.path):
1617 for line in file(self.path):
1618 t = line.rstrip().split(' ')
1620 self.apks[t[0]] = (t[1], None)
1622 self.apks[t[0]] = (t[1], time.strptime(t[2], '%Y-%m-%d'))
1623 self.changed = False
1625 def writeifchanged(self):
1627 if not os.path.exists('stats'):
1629 f = open(self.path, 'w')
1631 for apk, app in self.apks.iteritems():
1633 line = apk + ' ' + appid
1635 line += ' ' + time.strftime('%Y-%m-%d', added)
1637 for line in sorted(lst):
1638 f.write(line + '\n')
1641 # Record an apk (if it's new, otherwise does nothing)
1642 # Returns the date it was added.
1643 def recordapk(self, apk, app):
1644 if apk not in self.apks:
1645 self.apks[apk] = (app, time.gmtime(time.time()))
1647 _, added = self.apks[apk]
1650 # Look up information - given the 'apkname', returns (app id, date added/None).
1651 # Or returns None for an unknown apk.
1652 def getapp(self, apkname):
1653 if apkname in self.apks:
1654 return self.apks[apkname]
1657 # Get the most recent 'num' apps added to the repo, as a list of package ids
1658 # with the most recent first.
1659 def getlatest(self, num):
1661 for apk, app in self.apks.iteritems():
1665 if apps[appid] > added:
1669 sortedapps = sorted(apps.iteritems(), key=operator.itemgetter(1))[-num:]
1670 lst = [app for app, _ in sortedapps]
1675 def isApkDebuggable(apkfile, config):
1676 """Returns True if the given apk file is debuggable
1678 :param apkfile: full path to the apk to check"""
1680 p = SdkToolsPopen(['aapt', 'dump', 'xmltree', apkfile, 'AndroidManifest.xml'],
1682 if p.returncode != 0:
1683 logging.critical("Failed to get apk manifest information")
1685 for line in p.output.splitlines():
1686 if 'android:debuggable' in line and not line.endswith('0x0'):
1691 class AsynchronousFileReader(threading.Thread):
1694 Helper class to implement asynchronous reading of a file
1695 in a separate thread. Pushes read lines on a queue to
1696 be consumed in another thread.
1699 def __init__(self, fd, queue):
1700 assert isinstance(queue, Queue.Queue)
1701 assert callable(fd.readline)
1702 threading.Thread.__init__(self)
1707 '''The body of the tread: read lines and put them on the queue.'''
1708 for line in iter(self._fd.readline, ''):
1709 self._queue.put(line)
1712 '''Check whether there is no more content to expect.'''
1713 return not self.is_alive() and self._queue.empty()
1721 def SdkToolsPopen(commands, cwd=None, output=True):
1723 if cmd not in config:
1724 config[cmd] = find_sdk_tools_cmd(commands[0])
1725 return FDroidPopen([config[cmd]] + commands[1:],
1726 cwd=cwd, output=output)
1729 def FDroidPopen(commands, cwd=None, output=True):
1731 Run a command and capture the possibly huge output.
1733 :param commands: command and argument list like in subprocess.Popen
1734 :param cwd: optionally specifies a working directory
1735 :returns: A PopenResult.
1741 cwd = os.path.normpath(cwd)
1742 logging.debug("Directory: %s" % cwd)
1743 logging.debug("> %s" % ' '.join(commands))
1745 result = PopenResult()
1748 p = subprocess.Popen(commands, cwd=cwd, shell=False, env=env,
1749 stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
1751 raise BuildException("OSError while trying to execute " +
1752 ' '.join(commands) + ': ' + str(e))
1754 stdout_queue = Queue.Queue()
1755 stdout_reader = AsynchronousFileReader(p.stdout, stdout_queue)
1756 stdout_reader.start()
1758 # Check the queue for output (until there is no more to get)
1759 while not stdout_reader.eof():
1760 while not stdout_queue.empty():
1761 line = stdout_queue.get()
1762 if output and options.verbose:
1763 # Output directly to console
1764 sys.stderr.write(line)
1766 result.output += line
1770 result.returncode = p.wait()
1774 def remove_signing_keys(build_dir):
1775 comment = re.compile(r'[ ]*//')
1776 signing_configs = re.compile(r'^[\t ]*signingConfigs[ \t]*{[ \t]*$')
1778 re.compile(r'^[\t ]*signingConfig [^ ]*$'),
1779 re.compile(r'.*android\.signingConfigs\.[^{]*$'),
1780 re.compile(r'.*variant\.outputFile = .*'),
1781 re.compile(r'.*output\.outputFile = .*'),
1782 re.compile(r'.*\.readLine\(.*'),
1784 for root, dirs, files in os.walk(build_dir):
1785 if 'build.gradle' in files:
1786 path = os.path.join(root, 'build.gradle')
1788 with open(path, "r") as o:
1789 lines = o.readlines()
1795 with open(path, "w") as o:
1796 while i < len(lines):
1799 while line.endswith('\\\n'):
1800 line = line.rstrip('\\\n') + lines[i]
1803 if comment.match(line):
1807 opened += line.count('{')
1808 opened -= line.count('}')
1811 if signing_configs.match(line):
1816 if any(s.match(line) for s in line_matches):
1824 logging.info("Cleaned build.gradle of keysigning configs at %s" % path)
1827 'project.properties',
1829 'default.properties',
1830 'ant.properties', ]:
1831 if propfile in files:
1832 path = os.path.join(root, propfile)
1834 with open(path, "r") as o:
1835 lines = o.readlines()
1839 with open(path, "w") as o:
1841 if any(line.startswith(s) for s in ('key.store', 'key.alias')):
1848 logging.info("Cleaned %s of keysigning configs at %s" % (propfile, path))
1851 def reset_env_path():
1852 global env, orig_path
1853 env['PATH'] = orig_path
1856 def add_to_env_path(path):
1858 paths = env['PATH'].split(os.pathsep)
1862 env['PATH'] = os.pathsep.join(paths)
1865 def replace_config_vars(cmd):
1867 cmd = cmd.replace('$$SDK$$', config['sdk_path'])
1868 # env['ANDROID_NDK'] is set in build_local right before prepare_source
1869 cmd = cmd.replace('$$NDK$$', env['ANDROID_NDK'])
1870 cmd = cmd.replace('$$MVN3$$', config['mvn3'])
1874 def place_srclib(root_dir, number, libpath):
1877 relpath = os.path.relpath(libpath, root_dir)
1878 proppath = os.path.join(root_dir, 'project.properties')
1881 if os.path.isfile(proppath):
1882 with open(proppath, "r") as o:
1883 lines = o.readlines()
1885 with open(proppath, "w") as o:
1888 if line.startswith('android.library.reference.%d=' % number):
1889 o.write('android.library.reference.%d=%s\n' % (number, relpath))
1894 o.write('android.library.reference.%d=%s\n' % (number, relpath))
1897 def compare_apks(apk1, apk2, tmp_dir):
1900 Returns None if the apk content is the same (apart from the signing key),
1901 otherwise a string describing what's different, or what went wrong when
1902 trying to do the comparison.
1905 badchars = re.compile('''[/ :;'"]''')
1906 apk1dir = os.path.join(tmp_dir, badchars.sub('_', apk1[0:-4])) # trim .apk
1907 apk2dir = os.path.join(tmp_dir, badchars.sub('_', apk2[0:-4])) # trim .apk
1908 for d in [apk1dir, apk2dir]:
1909 if os.path.exists(d):
1912 os.mkdir(os.path.join(d, 'jar-xf'))
1914 if subprocess.call(['jar', 'xf',
1915 os.path.abspath(apk1)],
1916 cwd=os.path.join(apk1dir, 'jar-xf')) != 0:
1917 return("Failed to unpack " + apk1)
1918 if subprocess.call(['jar', 'xf',
1919 os.path.abspath(apk2)],
1920 cwd=os.path.join(apk2dir, 'jar-xf')) != 0:
1921 return("Failed to unpack " + apk2)
1923 # try to find apktool in the path, if it hasn't been manually configed
1924 if 'apktool' not in config:
1925 tmp = find_command('apktool')
1927 config['apktool'] = tmp
1928 if 'apktool' in config:
1929 if subprocess.call([config['apktool'], 'd', os.path.abspath(apk1), '--output', 'apktool'],
1931 return("Failed to unpack " + apk1)
1932 if subprocess.call([config['apktool'], 'd', os.path.abspath(apk2), '--output', 'apktool'],
1934 return("Failed to unpack " + apk2)
1936 p = FDroidPopen(['diff', '-r', apk1dir, apk2dir], output=False)
1937 lines = p.output.splitlines()
1938 if len(lines) != 1 or 'META-INF' not in lines[0]:
1939 meld = find_command('meld')
1940 if meld is not None:
1941 p = FDroidPopen(['meld', apk1dir, apk2dir], output=False)
1942 return("Unexpected diff output - " + p.output)
1944 # since everything verifies, delete the comparison to keep cruft down
1945 shutil.rmtree(apk1dir)
1946 shutil.rmtree(apk2dir)
1948 # If we get here, it seems like they're the same!
1952 def find_command(command):
1953 '''find the full path of a command, or None if it can't be found in the PATH'''
1956 return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
1958 fpath, fname = os.path.split(command)
1963 for path in os.environ["PATH"].split(os.pathsep):
1964 path = path.strip('"')
1965 exe_file = os.path.join(path, command)
1966 if is_exe(exe_file):