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/>.
36 def read_config(opts, config_file='config.py'):
37 """Read the repository config
39 The config is read from config_file, which is in the current directory when
40 any of the repo management commands are used.
42 global config, options
44 if config is not None:
46 if not os.path.isfile(config_file):
47 logging.critical("Missing config file - is this a repo directory?")
54 logging.debug("Reading %s" % config_file)
55 execfile(config_file, config)
58 'sdk_path': "$ANDROID_HOME",
59 'ndk_path': "$ANDROID_NDK",
60 'build_tools': "19.0.3",
65 'update_stats': False,
66 'stats_to_carbon': False,
68 'build_server_always': False,
69 'keystore': os.path.join(os.getenv('HOME'),
70 '.local', 'share', 'fdroidserver', 'keystore.jks'),
77 for k, v in defconfig.items():
81 # Expand environment variables
82 for k, v in config.items():
85 v = os.path.expanduser(v)
86 config[k] = os.path.expandvars(v)
88 if not config['sdk_path']:
89 logging.critical("$ANDROID_HOME is not set!")
91 if not os.path.isdir(config['sdk_path']):
92 logging.critical("$ANDROID_HOME points to a non-existing directory!")
95 if any(k in config for k in ["keystore", "keystorepass", "keypass"]):
96 st = os.stat(config_file)
97 if st.st_mode & stat.S_IRWXG or st.st_mode & stat.S_IRWXO:
98 logging.warn("unsafe permissions on {0} (should be 0600)!".format(config_file))
100 for k in ["keystorepass", "keypass"]:
102 write_password_file(k)
106 def write_password_file(pwtype, password=None):
108 writes out passwords to a protected file instead of passing passwords as
109 command line argments
111 filename = '.fdroid.' + pwtype + '.txt'
112 fd = os.open(filename, os.O_CREAT | os.O_WRONLY, 0600)
114 os.write(fd, config[pwtype])
116 os.write(fd, password)
118 config[pwtype + 'file'] = filename
120 # Given the arguments in the form of multiple appid:[vc] strings, this returns
121 # a dictionary with the set of vercodes specified for each package.
122 def read_pkg_args(args, allow_vercodes=False):
129 if allow_vercodes and ':' in p:
130 package, vercode = p.split(':')
132 package, vercode = p, None
133 if package not in vercodes:
134 vercodes[package] = [vercode] if vercode else []
136 elif vercode and vercode not in vercodes[package]:
137 vercodes[package] += [vercode] if vercode else []
141 # On top of what read_pkg_args does, this returns the whole app metadata, but
142 # limiting the builds list to the builds matching the vercodes specified.
143 def read_app_args(args, allapps, allow_vercodes=False):
145 vercodes = read_pkg_args(args, allow_vercodes)
150 apps = [app for app in allapps if app['id'] in vercodes]
152 if len(apps) != len(vercodes):
153 allids = [app["id"] for app in allapps]
156 logging.critical("No such package: %s" % p)
157 raise Exception("Found invalid app ids in arguments")
159 raise Exception("No packages specified")
163 vc = vercodes[app['id']]
166 app['builds'] = [b for b in app['builds'] if b['vercode'] in vc]
167 if len(app['builds']) != len(vercodes[app['id']]):
169 allvcs = [b['vercode'] for b in app['builds']]
170 for v in vercodes[app['id']]:
172 logging.critical("No such vercode %s for app %s" % (v, app['id']))
175 raise Exception("Found invalid vercodes for some apps")
179 def has_extension(filename, extension):
180 name, ext = os.path.splitext(filename)
181 ext = ext.lower()[1:]
182 return ext == extension
186 def apknameinfo(filename):
188 filename = os.path.basename(filename)
189 if apk_regex is None:
190 apk_regex = re.compile(r"^(.+)_([0-9]+)\.apk$")
191 m = apk_regex.match(filename)
193 result = (m.group(1), m.group(2))
194 except AttributeError:
195 raise Exception("Invalid apk name: %s" % filename)
198 def getapkname(app, build):
199 return "%s_%s.apk" % (app['id'], build['vercode'])
201 def getsrcname(app, build):
202 return "%s_%s_src.tar.gz" % (app['id'], build['vercode'])
208 return app['Auto Name']
212 return '%s (%s)' % (app['Current Version'], app['Current Version Code'])
214 def getvcs(vcstype, remote, local):
216 return vcs_git(remote, local)
218 return vcs_svn(remote, local)
219 if vcstype == 'git-svn':
220 return vcs_gitsvn(remote, local)
222 return vcs_hg(remote, local)
224 return vcs_bzr(remote, local)
225 if vcstype == 'srclib':
226 if local != 'build/srclib/' + remote:
227 raise VCSException("Error: srclib paths are hard-coded!")
228 return getsrclib(remote, 'build/srclib', raw=True)
229 raise VCSException("Invalid vcs type " + vcstype)
231 def getsrclibvcs(name):
232 srclib_path = os.path.join('srclibs', name + ".txt")
233 if not os.path.exists(srclib_path):
234 raise VCSException("Missing srclib " + name)
235 return metadata.parse_srclib(srclib_path)['Repo Type']
238 def __init__(self, remote, local):
240 # svn, git-svn and bzr may require auth
242 if self.repotype() in ('svn', 'git-svn', 'bzr'):
244 self.username, remote = remote.split('@')
245 if ':' not in self.username:
246 raise VCSException("Password required with username")
247 self.username, self.password = self.username.split(':')
251 self.refreshed = False
254 # Take the local repository to a clean version of the given revision, which
255 # is specificed in the VCS's native format. Beforehand, the repository can
256 # be dirty, or even non-existent. If the repository does already exist
257 # locally, it will be updated from the origin, but only once in the
258 # lifetime of the vcs object.
259 # None is acceptable for 'rev' if you know you are cloning a clean copy of
260 # the repo - otherwise it must specify a valid revision.
261 def gotorevision(self, rev):
263 # The .fdroidvcs-id file for a repo tells us what VCS type
264 # and remote that directory was created from, allowing us to drop it
265 # automatically if either of those things changes.
266 fdpath = os.path.join(self.local, '..',
267 '.fdroidvcs-' + os.path.basename(self.local))
268 cdata = self.repotype() + ' ' + self.remote
271 if os.path.exists(self.local):
272 if os.path.exists(fdpath):
273 with open(fdpath, 'r') as f:
274 fsdata = f.read().strip()
279 logging.info("Repository details changed - deleting")
282 logging.info("Repository details missing - deleting")
284 shutil.rmtree(self.local)
286 self.gotorevisionx(rev)
288 # If necessary, write the .fdroidvcs file.
290 with open(fdpath, 'w') as f:
293 # Derived classes need to implement this. It's called once basic checking
294 # has been performend.
295 def gotorevisionx(self, rev):
296 raise VCSException("This VCS type doesn't define gotorevisionx")
298 # Initialise and update submodules
299 def initsubmodules(self):
300 raise VCSException('Submodules not supported for this vcs type')
302 # Get a list of all known tags
304 raise VCSException('gettags not supported for this vcs type')
306 # Get current commit reference (hash, revision, etc)
308 raise VCSException('getref not supported for this vcs type')
310 # Returns the srclib (name, path) used in setting up the current
320 # If the local directory exists, but is somehow not a git repository, git
321 # will traverse up the directory tree until it finds one that is (i.e.
322 # fdroidserver) and then we'll proceed to destroy it! This is called as
325 p = SilentPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local)
326 result = p.stdout.rstrip()
327 if not result.endswith(self.local):
328 raise VCSException('Repository mismatch')
330 def gotorevisionx(self, rev):
331 if not os.path.exists(self.local):
333 p = FDroidPopen(['git', 'clone', self.remote, self.local])
334 if p.returncode != 0:
335 raise VCSException("Git clone failed")
339 # Discard any working tree changes
340 p = SilentPopen(['git', 'reset', '--hard'], cwd=self.local)
341 if p.returncode != 0:
342 raise VCSException("Git reset failed")
343 # Remove untracked files now, in case they're tracked in the target
344 # revision (it happens!)
345 p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
346 if p.returncode != 0:
347 raise VCSException("Git clean failed")
348 if not self.refreshed:
349 # Get latest commits and tags from remote
350 p = FDroidPopen(['git', 'fetch', 'origin'], cwd=self.local)
351 if p.returncode != 0:
352 raise VCSException("Git fetch failed")
353 p = SilentPopen(['git', 'fetch', '--prune', '--tags', 'origin'], cwd=self.local)
354 if p.returncode != 0:
355 raise VCSException("Git fetch failed")
356 self.refreshed = True
357 # Check out the appropriate revision
358 rev = str(rev if rev else 'origin/master')
359 p = SilentPopen(['git', 'checkout', '-f', rev], cwd=self.local)
360 if p.returncode != 0:
361 raise VCSException("Git checkout failed")
362 # Get rid of any uncontrolled files left behind
363 p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
364 if p.returncode != 0:
365 raise VCSException("Git clean failed")
367 def initsubmodules(self):
369 submfile = os.path.join(self.local, '.gitmodules')
370 if not os.path.isfile(submfile):
371 raise VCSException("No git submodules available")
373 # fix submodules not accessible without an account and public key auth
374 with open(submfile, 'r') as f:
375 lines = f.readlines()
376 with open(submfile, 'w') as f:
378 if 'git@github.com' in line:
379 line = line.replace('git@github.com:', 'https://github.com/')
383 ['git', 'reset', '--hard'],
384 ['git', 'clean', '-dffx'],
386 p = SilentPopen(['git', 'submodule', 'foreach', '--recursive'] + cmd, cwd=self.local)
387 if p.returncode != 0:
388 raise VCSException("Git submodule reset failed")
389 p = FDroidPopen(['git', 'submodule', 'update', '--init', '--force', '--recursive'], cwd=self.local)
390 if p.returncode != 0:
391 raise VCSException("Git submodule update failed")
395 p = SilentPopen(['git', 'tag'], cwd=self.local)
396 return p.stdout.splitlines()
399 class vcs_gitsvn(vcs):
404 # Damn git-svn tries to use a graphical password prompt, so we have to
405 # trick it into taking the password from stdin
407 if self.username is None:
409 return ('echo "%s" | DISPLAY="" ' % self.password, ' --username "%s"' % self.username)
411 # If the local directory exists, but is somehow not a git repository, git
412 # will traverse up the directory tree until it finds one that is (i.e.
413 # fdroidserver) and then we'll proceed to destory it! This is called as
416 p = SilentPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local)
417 result = p.stdout.rstrip()
418 if not result.endswith(self.local):
419 raise VCSException('Repository mismatch')
421 def gotorevisionx(self, rev):
422 if not os.path.exists(self.local):
424 gitsvn_cmd = '%sgit svn clone%s' % self.userargs()
425 if ';' in self.remote:
426 remote_split = self.remote.split(';')
427 for i in remote_split[1:]:
428 if i.startswith('trunk='):
429 gitsvn_cmd += ' -T %s' % i[6:]
430 elif i.startswith('tags='):
431 gitsvn_cmd += ' -t %s' % i[5:]
432 elif i.startswith('branches='):
433 gitsvn_cmd += ' -b %s' % i[9:]
434 p = SilentPopen([gitsvn_cmd + " %s %s" % (remote_split[0], self.local)], shell=True)
435 if p.returncode != 0:
436 raise VCSException("Git clone failed")
438 p = SilentPopen([gitsvn_cmd + " %s %s" % (self.remote, self.local)], shell=True)
439 if p.returncode != 0:
440 raise VCSException("Git clone failed")
444 # Discard any working tree changes
445 p = SilentPopen(['git', 'reset', '--hard'], cwd=self.local)
446 if p.returncode != 0:
447 raise VCSException("Git reset failed")
448 # Remove untracked files now, in case they're tracked in the target
449 # revision (it happens!)
450 p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
451 if p.returncode != 0:
452 raise VCSException("Git clean failed")
453 if not self.refreshed:
454 # Get new commits, branches and tags from repo
455 p = SilentPopen(['%sgit svn fetch %s' % self.userargs()], cwd=self.local, shell=True)
456 if p.returncode != 0:
457 raise VCSException("Git svn fetch failed")
458 p = SilentPopen(['%sgit svn rebase %s' % self.userargs()], cwd=self.local, shell=True)
459 if p.returncode != 0:
460 raise VCSException("Git svn rebase failed")
461 self.refreshed = True
463 rev = str(rev if rev else 'master')
465 nospaces_rev = rev.replace(' ', '%20')
466 # Try finding a svn tag
467 p = SilentPopen(['git', 'checkout', 'tags/' + nospaces_rev], cwd=self.local)
468 if p.returncode != 0:
469 # No tag found, normal svn rev translation
470 # Translate svn rev into git format
471 rev_split = rev.split('/')
472 if len(rev_split) > 1:
473 treeish = rev_split[0]
474 svn_rev = rev_split[1]
477 # if no branch is specified, then assume trunk (ie. 'master'
482 p = SilentPopen(['git', 'svn', 'find-rev', 'r' + svn_rev, treeish], cwd=self.local)
483 git_rev = p.stdout.rstrip()
485 if p.returncode != 0 or not git_rev:
486 # Try a plain git checkout as a last resort
487 p = SilentPopen(['git', 'checkout', rev], cwd=self.local)
488 if p.returncode != 0:
489 raise VCSException("No git treeish found and direct git checkout failed")
491 # Check out the git rev equivalent to the svn rev
492 p = SilentPopen(['git', 'checkout', git_rev], cwd=self.local)
493 if p.returncode != 0:
494 raise VCSException("Git svn checkout failed")
496 # Get rid of any uncontrolled files left behind
497 p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
498 if p.returncode != 0:
499 raise VCSException("Git clean failed")
503 return os.listdir(os.path.join(self.local, '.git/svn/refs/remotes/tags'))
507 p = SilentPopen(['git', 'svn', 'find-rev', 'HEAD'], cwd=self.local)
508 if p.returncode != 0:
510 return p.stdout.strip()
518 if self.username is None:
519 return ['--non-interactive']
520 return ['--username', self.username,
521 '--password', self.password,
524 def gotorevisionx(self, rev):
525 if not os.path.exists(self.local):
526 p = SilentPopen(['svn', 'checkout', self.remote, self.local] + self.userargs())
527 if p.returncode != 0:
528 raise VCSException("Svn checkout failed")
532 r"svn status | awk '/\?/ {print $2}' | xargs rm -rf"):
533 p = SilentPopen([svncommand], cwd=self.local, shell=True)
534 if p.returncode != 0:
535 raise VCSException("Svn reset ({0}) failed in {1}".format(svncommand, self.local))
536 if not self.refreshed:
537 p = SilentPopen(['svn', 'update'] + self.userargs(), cwd=self.local)
538 if p.returncode != 0:
539 raise VCSException("Svn update failed")
540 self.refreshed = True
542 revargs = list(['-r', rev] if rev else [])
543 p = SilentPopen(['svn', 'update', '--force'] + revargs + self.userargs(), cwd=self.local)
544 if p.returncode != 0:
545 raise VCSException("Svn update failed")
548 p = SilentPopen(['svn', 'info'], cwd=self.local)
549 for line in p.stdout.splitlines():
550 if line and line.startswith('Last Changed Rev: '):
559 def gotorevisionx(self, rev):
560 if not os.path.exists(self.local):
561 p = SilentPopen(['hg', 'clone', self.remote, self.local])
562 if p.returncode != 0:
563 raise VCSException("Hg clone failed")
565 p = SilentPopen(['hg status -uS | xargs rm -rf'], cwd=self.local, shell=True)
566 if p.returncode != 0:
567 raise VCSException("Hg clean failed")
568 if not self.refreshed:
569 p = SilentPopen(['hg', 'pull'], cwd=self.local)
570 if p.returncode != 0:
571 raise VCSException("Hg pull failed")
572 self.refreshed = True
574 rev = str(rev if rev else 'default')
577 p = SilentPopen(['hg', 'update', '-C', rev], cwd=self.local)
578 if p.returncode != 0:
579 raise VCSException("Hg checkout failed")
580 p = SilentPopen(['hg', 'purge', '--all'], cwd=self.local)
581 # Also delete untracked files, we have to enable purge extension for that:
582 if "'purge' is provided by the following extension" in p.stdout:
583 with open(self.local+"/.hg/hgrc", "a") as myfile:
584 myfile.write("\n[extensions]\nhgext.purge=\n")
585 p = SilentPopen(['hg', 'purge', '--all'], cwd=self.local)
586 if p.returncode != 0:
587 raise VCSException("HG purge failed")
588 elif p.returncode != 0:
589 raise VCSException("HG purge failed")
592 p = SilentPopen(['hg', 'tags', '-q'], cwd=self.local)
593 return p.stdout.splitlines()[1:]
601 def gotorevisionx(self, rev):
602 if not os.path.exists(self.local):
603 p = SilentPopen(['bzr', 'branch', self.remote, self.local])
604 if p.returncode != 0:
605 raise VCSException("Bzr branch failed")
607 p = SilentPopen(['bzr', 'clean-tree', '--force', '--unknown', '--ignored'], cwd=self.local)
608 if p.returncode != 0:
609 raise VCSException("Bzr revert failed")
610 if not self.refreshed:
611 p = SilentPopen(['bzr', 'pull'], cwd=self.local)
612 if p.returncode != 0:
613 raise VCSException("Bzr update failed")
614 self.refreshed = True
616 revargs = list(['-r', rev] if rev else [])
617 p = SilentPopen(['bzr', 'revert'] + revargs, cwd=self.local)
618 if p.returncode != 0:
619 raise VCSException("Bzr revert failed")
622 p = SilentPopen(['bzr', 'tags'], cwd=self.local)
623 return [tag.split(' ')[0].strip() for tag in
624 p.stdout.splitlines()]
626 def retrieve_string(app_dir, string, xmlfiles=None):
629 os.path.join(app_dir, 'res'),
630 os.path.join(app_dir, 'src/main/res'),
635 for res_dir in res_dirs:
636 for r,d,f in os.walk(res_dir):
637 if r.endswith('/values'):
638 xmlfiles += [os.path.join(r,x) for x in f if x.endswith('.xml')]
641 if string.startswith('@string/'):
642 string_search = re.compile(r'.*"'+string[8:]+'".*?>([^<]+?)<.*').search
643 elif string.startswith('&') and string.endswith(';'):
644 string_search = re.compile(r'.*<!ENTITY.*'+string[1:-1]+'.*?"([^"]+?)".*>').search
646 if string_search is not None:
647 for xmlfile in xmlfiles:
648 for line in file(xmlfile):
649 matches = string_search(line)
651 return retrieve_string(app_dir, matches.group(1), xmlfiles)
654 return string.replace("\\'","'")
656 # Return list of existing files that will be used to find the highest vercode
657 def manifest_paths(app_dir, flavour):
659 possible_manifests = [ os.path.join(app_dir, 'AndroidManifest.xml'),
660 os.path.join(app_dir, 'src', 'main', 'AndroidManifest.xml'),
661 os.path.join(app_dir, 'src', 'AndroidManifest.xml'),
662 os.path.join(app_dir, 'build.gradle') ]
665 possible_manifests.append(
666 os.path.join(app_dir, 'src', flavour, 'AndroidManifest.xml'))
668 return [path for path in possible_manifests if os.path.isfile(path)]
670 # Retrieve the package name. Returns the name, or None if not found.
671 def fetch_real_name(app_dir, flavour):
672 app_search = re.compile(r'.*<application.*').search
673 name_search = re.compile(r'.*android:label="([^"]+)".*').search
675 for f in manifest_paths(app_dir, flavour):
676 if not has_extension(f, 'xml'):
678 logging.debug("fetch_real_name: Checking manifest at " + f)
684 matches = name_search(line)
686 stringname = matches.group(1)
687 logging.debug("fetch_real_name: using string " + stringname)
688 result = retrieve_string(app_dir, stringname)
690 result = result.strip()
694 # Retrieve the version name
695 def version_name(original, app_dir, flavour):
696 for f in manifest_paths(app_dir, flavour):
697 if not has_extension(f, 'xml'):
699 string = retrieve_string(app_dir, original)
704 def get_library_references(root_dir):
706 proppath = os.path.join(root_dir, 'project.properties')
707 if not os.path.isfile(proppath):
709 with open(proppath) as f:
710 for line in f.readlines():
711 if not line.startswith('android.library.reference.'):
713 path = line.split('=')[1].strip()
714 relpath = os.path.join(root_dir, path)
715 if not os.path.isdir(relpath):
717 logging.info("Found subproject at %s" % path)
718 libraries.append(path)
721 def ant_subprojects(root_dir):
722 subprojects = get_library_references(root_dir)
723 for subpath in subprojects:
724 subrelpath = os.path.join(root_dir, subpath)
725 for p in get_library_references(subrelpath):
726 relp = os.path.normpath(os.path.join(subpath,p))
727 if relp not in subprojects:
728 subprojects.insert(0, relp)
731 def remove_debuggable_flags(root_dir):
732 # Remove forced debuggable flags
733 logging.info("Removing debuggable flags")
734 for root, dirs, files in os.walk(root_dir):
735 if 'AndroidManifest.xml' in files:
736 path = os.path.join(root, 'AndroidManifest.xml')
737 p = FDroidPopen(['sed','-i', 's/android:debuggable="[^"]*"//g', path])
738 if p.returncode != 0:
739 raise BuildException("Failed to remove debuggable flags of %s" % path)
741 # Extract some information from the AndroidManifest.xml at the given path.
742 # Returns (version, vercode, package), any or all of which might be None.
743 # All values returned are strings.
744 def parse_androidmanifests(paths):
747 return (None, None, None)
749 vcsearch = re.compile(r'.*:versionCode="([0-9]+?)".*').search
750 vnsearch = re.compile(r'.*:versionName="([^"]+?)".*').search
751 psearch = re.compile(r'.*package="([^"]+)".*').search
753 vcsearch_g = re.compile(r'.*versionCode[ ]*[=]*[ ]*["\']*([0-9]+)["\']*').search
754 vnsearch_g = re.compile(r'.*versionName[ ]*[=]*[ ]*(["\'])((?:(?=(\\?))\3.)*?)\1.*').search
755 psearch_g = re.compile(r'.*packageName[ ]*[=]*[ ]*["\']([^"]+)["\'].*').search
763 gradle = has_extension(path, 'gradle')
766 # Remember package name, may be defined separately from version+vercode
767 package = max_package
769 for line in file(path):
772 matches = psearch_g(line)
774 matches = psearch(line)
776 package = matches.group(1)
779 matches = vnsearch_g(line)
781 matches = vnsearch(line)
783 version = matches.group(2 if gradle else 1)
786 matches = vcsearch_g(line)
788 matches = vcsearch(line)
790 vercode = matches.group(1)
792 # Better some package name than nothing
793 if max_package is None:
794 max_package = package
796 if max_vercode is None or (vercode is not None and vercode > max_vercode):
797 max_version = version
798 max_vercode = vercode
799 max_package = package
801 if max_version is None:
802 max_version = "Unknown"
804 return (max_version, max_vercode, max_package)
806 class BuildException(Exception):
807 def __init__(self, value, detail = None):
811 def get_wikitext(self):
812 ret = repr(self.value) + "\n"
816 txt = self.detail[-8192:] if len(self.detail) > 8192 else self.detail
822 ret = repr(self.value)
824 ret += "\n==== detail begin ====\n%s\n==== detail end ====" % self.detail.strip()
827 class VCSException(Exception):
828 def __init__(self, value):
832 return repr(self.value)
834 # Get the specified source library.
835 # Returns the path to it. Normally this is the path to be used when referencing
836 # it, which may be a subdirectory of the actual project. If you want the base
837 # directory of the project, pass 'basepath=True'.
838 def getsrclib(spec, srclib_dir, srclibpaths=[], subdir=None,
839 basepath=False, raw=False, prepare=True, preponly=False):
847 name, ref = spec.split('@')
849 number, name = name.split(':', 1)
851 name, subdir = name.split('/',1)
853 srclib_path = os.path.join('srclibs', name + ".txt")
855 if not os.path.exists(srclib_path):
856 raise BuildException('srclib ' + name + ' not found.')
858 srclib = metadata.parse_srclib(srclib_path)
860 sdir = os.path.join(srclib_dir, name)
863 vcs = getvcs(srclib["Repo Type"], srclib["Repo"], sdir)
864 vcs.srclib = (name, number, sdir)
866 vcs.gotorevision(ref)
873 libdir = os.path.join(sdir, subdir)
874 elif srclib["Subdir"]:
875 for subdir in srclib["Subdir"]:
876 libdir_candidate = os.path.join(sdir, subdir)
877 if os.path.exists(libdir_candidate):
878 libdir = libdir_candidate
884 if srclib["Srclibs"]:
886 for lib in srclib["Srclibs"].replace(';',',').split(','):
888 for t in srclibpaths:
893 raise BuildException('Missing recursive srclib %s for %s' % (
895 place_srclib(libdir, n, s_tuple[2])
898 remove_signing_keys(sdir)
899 remove_debuggable_flags(sdir)
903 if srclib["Prepare"]:
904 cmd = replace_config_vars(srclib["Prepare"])
906 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=libdir)
907 if p.returncode != 0:
908 raise BuildException("Error running prepare command for srclib %s"
914 return (name, number, libdir)
917 # Prepare the source code for a particular build
918 # 'vcs' - the appropriate vcs object for the application
919 # 'app' - the application details from the metadata
920 # 'build' - the build details from the metadata
921 # 'build_dir' - the path to the build directory, usually
923 # 'srclib_dir' - the path to the source libraries directory, usually
925 # 'extlib_dir' - the path to the external libraries directory, usually
927 # Returns the (root, srclibpaths) where:
928 # 'root' is the root directory, which may be the same as 'build_dir' or may
929 # be a subdirectory of it.
930 # 'srclibpaths' is information on the srclibs being used
931 def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=False):
933 # Optionally, the actual app source can be in a subdirectory
934 if 'subdir' in build:
935 root_dir = os.path.join(build_dir, build['subdir'])
939 # Get a working copy of the right revision
940 logging.info("Getting source for revision " + build['commit'])
941 vcs.gotorevision(build['commit'])
943 # Initialise submodules if requred
944 if build['submodules']:
945 logging.info("Initialising submodules")
948 # Check that a subdir (if we're using one) exists. This has to happen
949 # after the checkout, since it might not exist elsewhere
950 if not os.path.exists(root_dir):
951 raise BuildException('Missing subdir ' + root_dir)
953 # Run an init command if one is required
955 cmd = replace_config_vars(build['init'])
956 logging.info("Running 'init' commands in %s" % root_dir)
958 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
959 if p.returncode != 0:
960 raise BuildException("Error running init command for %s:%s" %
961 (app['id'], build['version']), p.stdout)
963 # Apply patches if any
965 for patch in build['patch']:
966 patch = patch.strip()
967 logging.info("Applying " + patch)
968 patch_path = os.path.join('metadata', app['id'], patch)
969 p = FDroidPopen(['patch', '-p1', '-i', os.path.abspath(patch_path)], cwd=build_dir)
970 if p.returncode != 0:
971 raise BuildException("Failed to apply patch %s" % patch_path)
973 # Get required source libraries
975 if 'srclibs' in build:
976 logging.info("Collecting source libraries")
977 for lib in build['srclibs']:
978 srclibpaths.append(getsrclib(lib, srclib_dir, srclibpaths,
981 for name, number, libpath in srclibpaths:
982 place_srclib(root_dir, int(number) if number else None, libpath)
984 basesrclib = vcs.getsrclib()
985 # If one was used for the main source, add that too.
987 srclibpaths.append(basesrclib)
989 # Update the local.properties file
990 localprops = [ os.path.join(build_dir, 'local.properties') ]
991 if 'subdir' in build:
992 localprops += [ os.path.join(root_dir, 'local.properties') ]
993 for path in localprops:
994 if not os.path.isfile(path):
996 logging.info("Updating properties file at %s" % path)
1001 # Fix old-fashioned 'sdk-location' by copying
1002 # from sdk.dir, if necessary
1003 if build['oldsdkloc']:
1004 sdkloc = re.match(r".*^sdk.dir=(\S+)$.*", props,
1006 props += "sdk-location=%s\n" % sdkloc
1008 props += "sdk.dir=%s\n" % config['sdk_path']
1009 props += "sdk-location=%s\n" % config['sdk_path']
1010 if 'ndk_path' in config:
1012 props += "ndk.dir=%s\n" % config['ndk_path']
1013 props += "ndk-location=%s\n" % config['ndk_path']
1014 # Add java.encoding if necessary
1015 if 'encoding' in build:
1016 props += "java.encoding=%s\n" % build['encoding']
1022 if build['type'] == 'gradle':
1023 flavour = build['gradle'].split('@')[0]
1024 if flavour in ['main', 'yes', '']:
1027 if 'target' in build:
1028 n = build["target"].split('-')[1]
1029 FDroidPopen(['sed', '-i',
1030 's@compileSdkVersion[ ]*[0-9]*@compileSdkVersion '+n+'@g',
1031 'build.gradle'], cwd=root_dir)
1032 if '@' in build['gradle']:
1033 gradle_dir = os.path.join(root_dir, build['gradle'].split('@',1)[1])
1034 gradle_dir = os.path.normpath(gradle_dir)
1035 FDroidPopen(['sed', '-i',
1036 's@compileSdkVersion[ ]*[0-9]*@compileSdkVersion '+n+'@g',
1037 'build.gradle'], cwd=gradle_dir)
1039 # Remove forced debuggable flags
1040 remove_debuggable_flags(root_dir)
1042 # Insert version code and number into the manifest if necessary
1043 if build['forceversion']:
1044 logging.info("Changing the version name")
1045 for path in manifest_paths(root_dir, flavour):
1046 if not os.path.isfile(path):
1048 if has_extension(path, 'xml'):
1049 p = SilentPopen(['sed', '-i',
1050 's/android:versionName="[^"]*"/android:versionName="' + build['version'] + '"/g',
1052 if p.returncode != 0:
1053 raise BuildException("Failed to amend manifest")
1054 elif has_extension(path, 'gradle'):
1055 p = SilentPopen(['sed', '-i',
1056 's/versionName[ ]*=[ ]*"[^"]*"/versionName = "' + build['version'] + '"/g',
1058 if p.returncode != 0:
1059 raise BuildException("Failed to amend build.gradle")
1060 if build['forcevercode']:
1061 logging.info("Changing the version code")
1062 for path in manifest_paths(root_dir, flavour):
1063 if not os.path.isfile(path):
1065 if has_extension(path, 'xml'):
1066 p = SilentPopen(['sed', '-i',
1067 's/android:versionCode="[^"]*"/android:versionCode="' + build['vercode'] + '"/g',
1069 if p.returncode != 0:
1070 raise BuildException("Failed to amend manifest")
1071 elif has_extension(path, 'gradle'):
1072 p = SilentPopen(['sed', '-i',
1073 's/versionCode[ ]*=[ ]*[0-9]*/versionCode = ' + build['vercode'] + '/g',
1075 if p.returncode != 0:
1076 raise BuildException("Failed to amend build.gradle")
1078 # Delete unwanted files
1080 for part in build['rm']:
1081 dest = os.path.join(build_dir, part)
1082 logging.info("Removing {0}".format(part))
1083 if os.path.lexists(dest):
1084 if os.path.islink(dest):
1085 SilentPopen(['unlink ' + dest], shell=True)
1087 SilentPopen(['rm -rf ' + dest], shell=True)
1089 logging.info("...but it didn't exist")
1091 remove_signing_keys(build_dir)
1093 # Add required external libraries
1094 if 'extlibs' in build:
1095 logging.info("Collecting prebuilt libraries")
1096 libsdir = os.path.join(root_dir, 'libs')
1097 if not os.path.exists(libsdir):
1099 for lib in build['extlibs']:
1101 logging.info("...installing extlib {0}".format(lib))
1102 libf = os.path.basename(lib)
1103 libsrc = os.path.join(extlib_dir, lib)
1104 if not os.path.exists(libsrc):
1105 raise BuildException("Missing extlib file {0}".format(libsrc))
1106 shutil.copyfile(libsrc, os.path.join(libsdir, libf))
1108 # Run a pre-build command if one is required
1109 if 'prebuild' in build:
1110 cmd = replace_config_vars(build['prebuild'])
1112 # Substitute source library paths into prebuild commands
1113 for name, number, libpath in srclibpaths:
1114 libpath = os.path.relpath(libpath, root_dir)
1115 cmd = cmd.replace('$$' + name + '$$', libpath)
1117 logging.info("Running 'prebuild' commands in %s" % root_dir)
1119 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
1120 if p.returncode != 0:
1121 raise BuildException("Error running prebuild command for %s:%s" %
1122 (app['id'], build['version']), p.stdout)
1124 updatemode = build.get('update', ['auto'])
1125 # Generate (or update) the ant build file, build.xml...
1126 if updatemode != ['no'] and build['type'] == 'ant':
1127 parms = [os.path.join(config['sdk_path'], 'tools', 'android'), 'update']
1128 lparms = parms + ['lib-project']
1129 parms = parms + ['project']
1131 if 'target' in build and build['target']:
1132 parms += ['-t', build['target']]
1133 lparms += ['-t', build['target']]
1134 if updatemode == ['auto']:
1135 update_dirs = ant_subprojects(root_dir) + ['.']
1137 update_dirs = updatemode
1139 for d in update_dirs:
1140 subdir = os.path.join(root_dir, d)
1142 print("Updating main project")
1143 cmd = parms + ['-p', d]
1145 print("Updating subproject %s" % d)
1146 cmd = lparms + ['-p', d]
1147 p = FDroidPopen(cmd, cwd=root_dir)
1148 # Check to see whether an error was returned without a proper exit
1149 # code (this is the case for the 'no target set or target invalid'
1151 if p.returncode != 0 or p.stdout.startswith("Error: "):
1152 raise BuildException("Failed to update project at %s" % d, p.stdout)
1153 # Clean update dirs via ant
1155 logging.info("Cleaning subproject %s" % d)
1156 p = FDroidPopen(['ant', 'clean'], cwd=subdir)
1158 return (root_dir, srclibpaths)
1160 # Scan the source code in the given directory (and all subdirectories)
1161 # and return a list of potential problems.
1162 def scan_source(build_dir, root_dir, thisbuild):
1166 # Common known non-free blobs (always lower case):
1167 usual_suspects = ['flurryagent',
1169 'libgoogleanalytics',
1170 'admob-sdk-android',
1172 'googleadmobadssdk',
1173 'google-play-services',
1177 'youtubeandroidplayerapi',
1182 def getpaths(field):
1184 if field not in thisbuild:
1186 for p in thisbuild[field]:
1190 elif p.startswith('./'):
1192 elif not p.startswith('/'):
1198 scanignore = getpaths('scanignore')
1199 scandelete = getpaths('scandelete')
1202 ms = magic.open(magic.MIME_TYPE)
1204 except AttributeError:
1208 for i in scanignore:
1209 if fd.startswith(i):
1214 for i in scandelete:
1215 if fd.startswith(i):
1219 def removeproblem(what, fd, fp):
1220 logging.info('Removing %s at %s' % (what, fd))
1223 def handleproblem(what, fd, fp):
1225 removeproblem(what, fd, fp)
1227 problems.append('Found %s at %s' % (what, fd))
1229 def warnproblem(what, fd, fp):
1230 logging.warn('Found %s at %s' % (what, fd))
1232 def insidedir(path, dirname):
1233 return path.endswith('/%s' % dirname) or '/%s/' % dirname in path
1235 # Iterate through all files in the source code
1236 for r,d,f in os.walk(build_dir):
1238 if any(insidedir(r, igndir) for igndir in ('.hg', '.git', '.svn')):
1243 # Path (relative) to the file
1244 fp = os.path.join(r, curfile)
1245 fd = fp[len(build_dir):]
1247 # Check if this file has been explicitly excluded from scanning
1251 for suspect in usual_suspects:
1252 if suspect in curfile.lower():
1253 handleproblem('usual supect', fd, fp)
1255 mime = magic.from_file(fp, mime=True) if ms is None else ms.file(fp)
1256 if mime == 'application/x-sharedlib':
1257 handleproblem('shared library', fd, fp)
1258 elif mime == 'application/x-archive':
1259 handleproblem('static library', fd, fp)
1260 elif mime == 'application/x-executable':
1261 handleproblem('binary executable', fd, fp)
1262 elif mime == 'application/x-java-applet':
1263 handleproblem('Java compiled class', fd, fp)
1264 elif mime == 'application/jar' and has_extension(fp, 'apk'):
1265 removeproblem('APK file', fd, fp)
1266 elif has_extension(fp, 'jar') and mime in [
1268 'application/java-archive',
1271 warnproblem('JAR file', fd, fp)
1272 elif mime == 'application/zip':
1273 warnproblem('ZIP file', fd, fp)
1275 elif has_extension(fp, 'java'):
1276 for line in file(fp):
1277 if 'DexClassLoader' in line:
1278 handleproblem('DexClassLoader', fd, fp)
1283 # Presence of a jni directory without buildjni=yes might
1284 # indicate a problem (if it's not a problem, explicitly use
1285 # buildjni=no to bypass this check)
1286 if (os.path.exists(os.path.join(root_dir, 'jni')) and
1287 thisbuild.get('buildjni') is None):
1288 msg = 'Found jni directory, but buildjni is not enabled'
1289 problems.append(msg)
1297 self.path = os.path.join('stats', 'known_apks.txt')
1299 if os.path.exists(self.path):
1300 for line in file( self.path):
1301 t = line.rstrip().split(' ')
1303 self.apks[t[0]] = (t[1], None)
1305 self.apks[t[0]] = (t[1], time.strptime(t[2], '%Y-%m-%d'))
1306 self.changed = False
1308 def writeifchanged(self):
1310 if not os.path.exists('stats'):
1312 f = open(self.path, 'w')
1314 for apk, app in self.apks.iteritems():
1316 line = apk + ' ' + appid
1318 line += ' ' + time.strftime('%Y-%m-%d', added)
1320 for line in sorted(lst):
1321 f.write(line + '\n')
1324 # Record an apk (if it's new, otherwise does nothing)
1325 # Returns the date it was added.
1326 def recordapk(self, apk, app):
1327 if not apk in self.apks:
1328 self.apks[apk] = (app, time.gmtime(time.time()))
1330 _, added = self.apks[apk]
1333 # Look up information - given the 'apkname', returns (app id, date added/None).
1334 # Or returns None for an unknown apk.
1335 def getapp(self, apkname):
1336 if apkname in self.apks:
1337 return self.apks[apkname]
1340 # Get the most recent 'num' apps added to the repo, as a list of package ids
1341 # with the most recent first.
1342 def getlatest(self, num):
1344 for apk, app in self.apks.iteritems():
1348 if apps[appid] > added:
1352 sortedapps = sorted(apps.iteritems(), key=operator.itemgetter(1))[-num:]
1353 lst = [app for app,added in sortedapps]
1357 def isApkDebuggable(apkfile, config):
1358 """Returns True if the given apk file is debuggable
1360 :param apkfile: full path to the apk to check"""
1362 p = SilentPopen([os.path.join(config['sdk_path'],
1363 'build-tools', config['build_tools'], 'aapt'),
1364 'dump', 'xmltree', apkfile, 'AndroidManifest.xml'])
1365 if p.returncode != 0:
1366 logging.critical("Failed to get apk manifest information")
1368 for line in p.stdout.splitlines():
1369 if 'android:debuggable' in line and not line.endswith('0x0'):
1374 class AsynchronousFileReader(threading.Thread):
1376 Helper class to implement asynchronous reading of a file
1377 in a separate thread. Pushes read lines on a queue to
1378 be consumed in another thread.
1381 def __init__(self, fd, queue):
1382 assert isinstance(queue, Queue.Queue)
1383 assert callable(fd.readline)
1384 threading.Thread.__init__(self)
1389 '''The body of the tread: read lines and put them on the queue.'''
1390 for line in iter(self._fd.readline, ''):
1391 self._queue.put(line)
1394 '''Check whether there is no more content to expect.'''
1395 return not self.is_alive() and self._queue.empty()
1401 def SilentPopen(commands, cwd=None, shell=False):
1402 return FDroidPopen(commands, cwd=cwd, shell=shell, output=False)
1404 def FDroidPopen(commands, cwd=None, shell=False, output=True):
1406 Run a command and capture the possibly huge output.
1408 :param commands: command and argument list like in subprocess.Popen
1409 :param cwd: optionally specifies a working directory
1410 :returns: A PopenResult.
1414 cwd = os.path.normpath(cwd)
1415 logging.info("Directory: %s" % cwd)
1416 logging.info("> %s" % ' '.join(commands))
1418 result = PopenResult()
1419 p = subprocess.Popen(commands, cwd=cwd, shell=shell,
1420 universal_newlines=True,
1421 stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
1423 stdout_queue = Queue.Queue()
1424 stdout_reader = AsynchronousFileReader(p.stdout, stdout_queue)
1425 stdout_reader.start()
1427 # Check the queue for output (until there is no more to get)
1428 while not stdout_reader.eof():
1429 while not stdout_queue.empty():
1430 line = stdout_queue.get()
1431 if output and options.verbose:
1432 # Output directly to console
1433 sys.stdout.write(line)
1435 result.stdout += line
1440 result.returncode = p.returncode
1443 def remove_signing_keys(build_dir):
1444 comment = re.compile(r'[ ]*//')
1445 signing_configs = re.compile(r'^[\t ]*signingConfigs[ \t]*{[ \t]*$')
1447 re.compile(r'^[\t ]*signingConfig [^ ]*$'),
1448 re.compile(r'.*android\.signingConfigs\..*'),
1449 re.compile(r'.*variant\.outputFile = .*'),
1450 re.compile(r'.*\.readLine\(.*'),
1452 for root, dirs, files in os.walk(build_dir):
1453 if 'build.gradle' in files:
1454 path = os.path.join(root, 'build.gradle')
1456 with open(path, "r") as o:
1457 lines = o.readlines()
1460 with open(path, "w") as o:
1462 if comment.match(line):
1466 opened += line.count('{')
1467 opened -= line.count('}')
1470 if signing_configs.match(line):
1474 if any(s.match(line) for s in line_matches):
1480 logging.info("Cleaned build.gradle of keysigning configs at %s" % path)
1483 'project.properties',
1485 'default.properties',
1488 if propfile in files:
1489 path = os.path.join(root, propfile)
1491 with open(path, "r") as o:
1492 lines = o.readlines()
1494 with open(path, "w") as o:
1496 if line.startswith('key.store'):
1498 if line.startswith('key.alias'):
1502 logging.info("Cleaned %s of keysigning configs at %s" % (propfile,path))
1504 def replace_config_vars(cmd):
1505 cmd = cmd.replace('$$SDK$$', config['sdk_path'])
1506 cmd = cmd.replace('$$NDK$$', config['ndk_path'])
1507 cmd = cmd.replace('$$MVN3$$', config['mvn3'])
1510 def place_srclib(root_dir, number, libpath):
1513 relpath = os.path.relpath(libpath, root_dir)
1514 proppath = os.path.join(root_dir, 'project.properties')
1517 if os.path.isfile(proppath):
1518 with open(proppath, "r") as o:
1519 lines = o.readlines()
1521 with open(proppath, "w") as o:
1524 if line.startswith('android.library.reference.%d=' % number):
1525 o.write('android.library.reference.%d=%s\n' % (number,relpath))
1530 o.write('android.library.reference.%d=%s\n' % (number,relpath))