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
41 def get_default_config():
43 'sdk_path': os.getenv("ANDROID_HOME"),
44 'ndk_path': os.getenv("ANDROID_NDK"),
45 'build_tools': "19.1.0",
50 'update_stats': False,
51 'stats_to_carbon': False,
53 'build_server_always': False,
54 'keystore': os.path.join(os.getenv("HOME"), '.local', 'share', 'fdroidserver', 'keystore.jks'),
55 'smartcardoptions': [],
64 def read_config(opts, config_file='config.py'):
65 """Read the repository config
67 The config is read from config_file, which is in the current directory when
68 any of the repo management commands are used.
70 global config, options
72 if config is not None:
74 if not os.path.isfile(config_file):
75 logging.critical("Missing config file - is this a repo directory?")
82 logging.debug("Reading %s" % config_file)
83 execfile(config_file, config)
85 # smartcardoptions must be a list since its command line args for Popen
86 if 'smartcardoptions' in config:
87 config['smartcardoptions'] = config['smartcardoptions'].split(' ')
88 elif 'keystore' in config and config['keystore'] == 'NONE':
89 # keystore='NONE' means use smartcard, these are required defaults
90 config['smartcardoptions'] = ['-storetype', 'PKCS11', '-providerName',
91 'SunPKCS11-OpenSC', '-providerClass',
92 'sun.security.pkcs11.SunPKCS11',
93 '-providerArg', 'opensc-fdroid.cfg']
95 defconfig = get_default_config()
96 for k, v in defconfig.items():
100 # Expand environment variables
101 for k, v in config.items():
104 v = os.path.expanduser(v)
105 config[k] = os.path.expandvars(v)
107 if not test_sdk_exists(config):
110 if any(k in config for k in ["keystore", "keystorepass", "keypass"]):
111 st = os.stat(config_file)
112 if st.st_mode & stat.S_IRWXG or st.st_mode & stat.S_IRWXO:
113 logging.warn("unsafe permissions on {0} (should be 0600)!".format(config_file))
115 for k in ["keystorepass", "keypass"]:
117 write_password_file(k)
119 # since this is used with rsync, where trailing slashes have meaning,
120 # ensure there is always a trailing slash
121 if 'serverwebroot' in config:
122 if config['serverwebroot'][-1] != '/':
123 config['serverwebroot'] += '/'
124 config['serverwebroot'] = config['serverwebroot'].replace('//', '/')
129 def test_sdk_exists(c):
130 if c['sdk_path'] is None:
131 # c['sdk_path'] is set to the value of ANDROID_HOME by default
132 logging.critical('No Android SDK found! ANDROID_HOME is not set and sdk_path is not in config.py!')
133 logging.info('You can use ANDROID_HOME to set the path to your SDK, i.e.:')
134 logging.info('\texport ANDROID_HOME=/opt/android-sdk')
136 if not os.path.exists(c['sdk_path']):
137 logging.critical('Android SDK path "' + c['sdk_path'] + '" does not exist!')
139 if not os.path.isdir(c['sdk_path']):
140 logging.critical('Android SDK path "' + c['sdk_path'] + '" is not a directory!')
142 if not os.path.isdir(os.path.join(c['sdk_path'], 'build-tools')):
143 logging.critical('Android SDK path "' + c['sdk_path'] + '" does not contain "build-tools/"!')
145 if not os.path.isdir(os.path.join(c['sdk_path'], 'build-tools', c['build_tools'])):
146 logging.critical('Configured build-tools version "' + c['build_tools'] + '" not found in the SDK!')
151 def test_build_tools_exists(c):
152 if not test_sdk_exists(c):
154 build_tools = os.path.join(c['sdk_path'], 'build-tools')
155 versioned_build_tools = os.path.join(build_tools, c['build_tools'])
156 if not os.path.isdir(versioned_build_tools):
157 logging.critical('Android Build Tools path "'
158 + versioned_build_tools + '" does not exist!')
160 if not os.path.exists(os.path.join(c['sdk_path'], 'build-tools', c['build_tools'], 'aapt')):
161 logging.critical('Android Build Tools "'
162 + versioned_build_tools
163 + '" does not contain "aapt"!')
168 def write_password_file(pwtype, password=None):
170 writes out passwords to a protected file instead of passing passwords as
171 command line argments
173 filename = '.fdroid.' + pwtype + '.txt'
174 fd = os.open(filename, os.O_CREAT | os.O_TRUNC | os.O_WRONLY, 0600)
176 os.write(fd, config[pwtype])
178 os.write(fd, password)
180 config[pwtype + 'file'] = filename
183 # Given the arguments in the form of multiple appid:[vc] strings, this returns
184 # a dictionary with the set of vercodes specified for each package.
185 def read_pkg_args(args, allow_vercodes=False):
192 if allow_vercodes and ':' in p:
193 package, vercode = p.split(':')
195 package, vercode = p, None
196 if package not in vercodes:
197 vercodes[package] = [vercode] if vercode else []
199 elif vercode and vercode not in vercodes[package]:
200 vercodes[package] += [vercode] if vercode else []
205 # On top of what read_pkg_args does, this returns the whole app metadata, but
206 # limiting the builds list to the builds matching the vercodes specified.
207 def read_app_args(args, allapps, allow_vercodes=False):
209 vercodes = read_pkg_args(args, allow_vercodes)
214 apps = [app for app in allapps if app['id'] in vercodes]
216 if len(apps) != len(vercodes):
217 allids = [app["id"] for app in allapps]
220 logging.critical("No such package: %s" % p)
221 raise Exception("Found invalid app ids in arguments")
223 raise Exception("No packages specified")
227 vc = vercodes[app['id']]
230 app['builds'] = [b for b in app['builds'] if b['vercode'] in vc]
231 if len(app['builds']) != len(vercodes[app['id']]):
233 allvcs = [b['vercode'] for b in app['builds']]
234 for v in vercodes[app['id']]:
236 logging.critical("No such vercode %s for app %s" % (v, app['id']))
239 raise Exception("Found invalid vercodes for some apps")
244 def has_extension(filename, extension):
245 name, ext = os.path.splitext(filename)
246 ext = ext.lower()[1:]
247 return ext == extension
252 def apknameinfo(filename):
254 filename = os.path.basename(filename)
255 if apk_regex is None:
256 apk_regex = re.compile(r"^(.+)_([0-9]+)\.apk$")
257 m = apk_regex.match(filename)
259 result = (m.group(1), m.group(2))
260 except AttributeError:
261 raise Exception("Invalid apk name: %s" % filename)
265 def getapkname(app, build):
266 return "%s_%s.apk" % (app['id'], build['vercode'])
269 def getsrcname(app, build):
270 return "%s_%s_src.tar.gz" % (app['id'], build['vercode'])
277 return app['Auto Name']
282 return '%s (%s)' % (app['Current Version'], app['Current Version Code'])
285 def getvcs(vcstype, remote, local):
287 return vcs_git(remote, local)
289 return vcs_svn(remote, local)
290 if vcstype == 'git-svn':
291 return vcs_gitsvn(remote, local)
293 return vcs_hg(remote, local)
295 return vcs_bzr(remote, local)
296 if vcstype == 'srclib':
297 if local != 'build/srclib/' + remote:
298 raise VCSException("Error: srclib paths are hard-coded!")
299 return getsrclib(remote, 'build/srclib', raw=True)
300 raise VCSException("Invalid vcs type " + vcstype)
303 def getsrclibvcs(name):
304 if name not in metadata.srclibs:
305 raise VCSException("Missing srclib " + name)
306 return metadata.srclibs[name]['Repo Type']
310 def __init__(self, remote, local):
312 # svn, git-svn and bzr may require auth
314 if self.repotype() in ('svn', 'git-svn', 'bzr'):
316 self.username, remote = remote.split('@')
317 if ':' not in self.username:
318 raise VCSException("Password required with username")
319 self.username, self.password = self.username.split(':')
323 self.refreshed = False
329 # Take the local repository to a clean version of the given revision, which
330 # is specificed in the VCS's native format. Beforehand, the repository can
331 # be dirty, or even non-existent. If the repository does already exist
332 # locally, it will be updated from the origin, but only once in the
333 # lifetime of the vcs object.
334 # None is acceptable for 'rev' if you know you are cloning a clean copy of
335 # the repo - otherwise it must specify a valid revision.
336 def gotorevision(self, rev):
338 # The .fdroidvcs-id file for a repo tells us what VCS type
339 # and remote that directory was created from, allowing us to drop it
340 # automatically if either of those things changes.
341 fdpath = os.path.join(self.local, '..',
342 '.fdroidvcs-' + os.path.basename(self.local))
343 cdata = self.repotype() + ' ' + self.remote
346 if os.path.exists(self.local):
347 if os.path.exists(fdpath):
348 with open(fdpath, 'r') as f:
349 fsdata = f.read().strip()
354 logging.info("Repository details changed - deleting")
357 logging.info("Repository details missing - deleting")
359 shutil.rmtree(self.local)
361 self.gotorevisionx(rev)
363 # If necessary, write the .fdroidvcs file.
365 with open(fdpath, 'w') as f:
368 # Derived classes need to implement this. It's called once basic checking
369 # has been performend.
370 def gotorevisionx(self, rev):
371 raise VCSException("This VCS type doesn't define gotorevisionx")
373 # Initialise and update submodules
374 def initsubmodules(self):
375 raise VCSException('Submodules not supported for this vcs type')
377 # Get a list of all known tags
379 raise VCSException('gettags not supported for this vcs type')
381 # Get a list of latest number tags
382 def latesttags(self, number):
383 raise VCSException('latesttags not supported for this vcs type')
385 # Get current commit reference (hash, revision, etc)
387 raise VCSException('getref not supported for this vcs type')
389 # Returns the srclib (name, path) used in setting up the current
400 # If the local directory exists, but is somehow not a git repository, git
401 # will traverse up the directory tree until it finds one that is (i.e.
402 # fdroidserver) and then we'll proceed to destroy it! This is called as
405 p = SilentPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local)
406 result = p.stdout.rstrip()
407 if not result.endswith(self.local):
408 raise VCSException('Repository mismatch')
410 def gotorevisionx(self, rev):
411 if not os.path.exists(self.local):
413 p = FDroidPopen(['git', 'clone', self.remote, self.local])
414 if p.returncode != 0:
415 raise VCSException("Git clone failed")
419 # Discard any working tree changes
420 p = SilentPopen(['git', 'reset', '--hard'], cwd=self.local)
421 if p.returncode != 0:
422 raise VCSException("Git reset failed")
423 # Remove untracked files now, in case they're tracked in the target
424 # revision (it happens!)
425 p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
426 if p.returncode != 0:
427 raise VCSException("Git clean failed")
428 if not self.refreshed:
429 # Get latest commits and tags from remote
430 p = FDroidPopen(['git', 'fetch', 'origin'], cwd=self.local)
431 if p.returncode != 0:
432 raise VCSException("Git fetch failed")
433 p = SilentPopen(['git', 'fetch', '--prune', '--tags', 'origin'], cwd=self.local)
434 if p.returncode != 0:
435 raise VCSException("Git fetch failed")
436 self.refreshed = True
437 # Check out the appropriate revision
438 rev = str(rev if rev else 'origin/master')
439 p = SilentPopen(['git', 'checkout', '-f', rev], cwd=self.local)
440 if p.returncode != 0:
441 raise VCSException("Git checkout failed")
442 # Get rid of any uncontrolled files left behind
443 p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
444 if p.returncode != 0:
445 raise VCSException("Git clean failed")
447 def initsubmodules(self):
449 submfile = os.path.join(self.local, '.gitmodules')
450 if not os.path.isfile(submfile):
451 raise VCSException("No git submodules available")
453 # fix submodules not accessible without an account and public key auth
454 with open(submfile, 'r') as f:
455 lines = f.readlines()
456 with open(submfile, 'w') as f:
458 if 'git@github.com' in line:
459 line = line.replace('git@github.com:', 'https://github.com/')
463 ['git', 'reset', '--hard'],
464 ['git', 'clean', '-dffx'],
466 p = SilentPopen(['git', 'submodule', 'foreach', '--recursive'] + cmd, cwd=self.local)
467 if p.returncode != 0:
468 raise VCSException("Git submodule reset failed")
469 p = FDroidPopen(['git', 'submodule', 'sync'], cwd=self.local)
470 if p.returncode != 0:
471 raise VCSException("Git submodule sync failed")
472 p = FDroidPopen(['git', 'submodule', 'update', '--init', '--force', '--recursive'], cwd=self.local)
473 if p.returncode != 0:
474 raise VCSException("Git submodule update failed")
478 p = SilentPopen(['git', 'tag'], cwd=self.local)
479 return p.stdout.splitlines()
481 def latesttags(self, alltags, number):
483 p = SilentPopen(['echo "' + '\n'.join(alltags) + '" | \
484 xargs -I@ git log --format=format:"%at @%n" -1 @ | \
485 sort -n | awk \'{print $2}\''],
486 cwd=self.local, shell=True)
487 return p.stdout.splitlines()[-number:]
490 class vcs_gitsvn(vcs):
495 # Damn git-svn tries to use a graphical password prompt, so we have to
496 # trick it into taking the password from stdin
498 if self.username is None:
500 return ('echo "%s" | DISPLAY="" ' % self.password, ' --username "%s"' % self.username)
502 # If the local directory exists, but is somehow not a git repository, git
503 # will traverse up the directory tree until it finds one that is (i.e.
504 # fdroidserver) and then we'll proceed to destory it! This is called as
507 p = SilentPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local)
508 result = p.stdout.rstrip()
509 if not result.endswith(self.local):
510 raise VCSException('Repository mismatch')
512 def gotorevisionx(self, rev):
513 if not os.path.exists(self.local):
515 gitsvn_cmd = '%sgit svn clone%s' % self.userargs()
516 if ';' in self.remote:
517 remote_split = self.remote.split(';')
518 for i in remote_split[1:]:
519 if i.startswith('trunk='):
520 gitsvn_cmd += ' -T %s' % i[6:]
521 elif i.startswith('tags='):
522 gitsvn_cmd += ' -t %s' % i[5:]
523 elif i.startswith('branches='):
524 gitsvn_cmd += ' -b %s' % i[9:]
525 p = SilentPopen([gitsvn_cmd + " %s %s" % (remote_split[0], self.local)], shell=True)
526 if p.returncode != 0:
527 raise VCSException("Git clone failed")
529 p = SilentPopen([gitsvn_cmd + " %s %s" % (self.remote, self.local)], shell=True)
530 if p.returncode != 0:
531 raise VCSException("Git clone failed")
535 # Discard any working tree changes
536 p = SilentPopen(['git', 'reset', '--hard'], cwd=self.local)
537 if p.returncode != 0:
538 raise VCSException("Git reset failed")
539 # Remove untracked files now, in case they're tracked in the target
540 # revision (it happens!)
541 p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
542 if p.returncode != 0:
543 raise VCSException("Git clean failed")
544 if not self.refreshed:
545 # Get new commits, branches and tags from repo
546 p = SilentPopen(['%sgit svn fetch %s' % self.userargs()], cwd=self.local, shell=True)
547 if p.returncode != 0:
548 raise VCSException("Git svn fetch failed")
549 p = SilentPopen(['%sgit svn rebase %s' % self.userargs()], cwd=self.local, shell=True)
550 if p.returncode != 0:
551 raise VCSException("Git svn rebase failed")
552 self.refreshed = True
554 rev = str(rev if rev else 'master')
556 nospaces_rev = rev.replace(' ', '%20')
557 # Try finding a svn tag
558 p = SilentPopen(['git', 'checkout', 'tags/' + nospaces_rev], cwd=self.local)
559 if p.returncode != 0:
560 # No tag found, normal svn rev translation
561 # Translate svn rev into git format
562 rev_split = rev.split('/')
563 if len(rev_split) > 1:
564 treeish = rev_split[0]
565 svn_rev = rev_split[1]
568 # if no branch is specified, then assume trunk (ie. 'master'
573 p = SilentPopen(['git', 'svn', 'find-rev', 'r' + svn_rev, treeish], cwd=self.local)
574 git_rev = p.stdout.rstrip()
576 if p.returncode != 0 or not git_rev:
577 # Try a plain git checkout as a last resort
578 p = SilentPopen(['git', 'checkout', rev], cwd=self.local)
579 if p.returncode != 0:
580 raise VCSException("No git treeish found and direct git checkout failed")
582 # Check out the git rev equivalent to the svn rev
583 p = SilentPopen(['git', 'checkout', git_rev], cwd=self.local)
584 if p.returncode != 0:
585 raise VCSException("Git svn checkout failed")
587 # Get rid of any uncontrolled files left behind
588 p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
589 if p.returncode != 0:
590 raise VCSException("Git clean failed")
594 return os.listdir(os.path.join(self.local, '.git/svn/refs/remotes/tags'))
598 p = SilentPopen(['git', 'svn', 'find-rev', 'HEAD'], cwd=self.local)
599 if p.returncode != 0:
601 return p.stdout.strip()
610 if self.username is None:
611 return ['--non-interactive']
612 return ['--username', self.username,
613 '--password', self.password,
616 def gotorevisionx(self, rev):
617 if not os.path.exists(self.local):
618 p = SilentPopen(['svn', 'checkout', self.remote, self.local] + self.userargs())
619 if p.returncode != 0:
620 raise VCSException("Svn checkout failed")
624 r"svn status | awk '/\?/ {print $2}' | xargs rm -rf"):
625 p = SilentPopen([svncommand], cwd=self.local, shell=True)
626 if p.returncode != 0:
627 raise VCSException("Svn reset ({0}) failed in {1}".format(svncommand, self.local))
628 if not self.refreshed:
629 p = SilentPopen(['svn', 'update'] + self.userargs(), cwd=self.local)
630 if p.returncode != 0:
631 raise VCSException("Svn update failed")
632 self.refreshed = True
634 revargs = list(['-r', rev] if rev else [])
635 p = SilentPopen(['svn', 'update', '--force'] + revargs + self.userargs(), cwd=self.local)
636 if p.returncode != 0:
637 raise VCSException("Svn update failed")
640 p = SilentPopen(['svn', 'info'], cwd=self.local)
641 for line in p.stdout.splitlines():
642 if line and line.startswith('Last Changed Rev: '):
652 def gotorevisionx(self, rev):
653 if not os.path.exists(self.local):
654 p = SilentPopen(['hg', 'clone', self.remote, self.local])
655 if p.returncode != 0:
656 raise VCSException("Hg clone failed")
658 p = SilentPopen(['hg status -uS | xargs rm -rf'], cwd=self.local, shell=True)
659 if p.returncode != 0:
660 raise VCSException("Hg clean failed")
661 if not self.refreshed:
662 p = SilentPopen(['hg', 'pull'], cwd=self.local)
663 if p.returncode != 0:
664 raise VCSException("Hg pull failed")
665 self.refreshed = True
667 rev = str(rev if rev else 'default')
670 p = SilentPopen(['hg', 'update', '-C', rev], cwd=self.local)
671 if p.returncode != 0:
672 raise VCSException("Hg checkout failed")
673 p = SilentPopen(['hg', 'purge', '--all'], cwd=self.local)
674 # Also delete untracked files, we have to enable purge extension for that:
675 if "'purge' is provided by the following extension" in p.stdout:
676 with open(self.local + "/.hg/hgrc", "a") as myfile:
677 myfile.write("\n[extensions]\nhgext.purge=\n")
678 p = SilentPopen(['hg', 'purge', '--all'], cwd=self.local)
679 if p.returncode != 0:
680 raise VCSException("HG purge failed")
681 elif p.returncode != 0:
682 raise VCSException("HG purge failed")
685 p = SilentPopen(['hg', 'tags', '-q'], cwd=self.local)
686 return p.stdout.splitlines()[1:]
694 def gotorevisionx(self, rev):
695 if not os.path.exists(self.local):
696 p = SilentPopen(['bzr', 'branch', self.remote, self.local])
697 if p.returncode != 0:
698 raise VCSException("Bzr branch failed")
700 p = SilentPopen(['bzr', 'clean-tree', '--force', '--unknown', '--ignored'], cwd=self.local)
701 if p.returncode != 0:
702 raise VCSException("Bzr revert failed")
703 if not self.refreshed:
704 p = SilentPopen(['bzr', 'pull'], cwd=self.local)
705 if p.returncode != 0:
706 raise VCSException("Bzr update failed")
707 self.refreshed = True
709 revargs = list(['-r', rev] if rev else [])
710 p = SilentPopen(['bzr', 'revert'] + revargs, cwd=self.local)
711 if p.returncode != 0:
712 raise VCSException("Bzr revert failed")
715 p = SilentPopen(['bzr', 'tags'], cwd=self.local)
716 return [tag.split(' ')[0].strip() for tag in
717 p.stdout.splitlines()]
720 def retrieve_string(app_dir, string, xmlfiles=None):
723 os.path.join(app_dir, 'res'),
724 os.path.join(app_dir, 'src/main'),
729 for res_dir in res_dirs:
730 for r, d, f in os.walk(res_dir):
731 if r.endswith('/values'):
732 xmlfiles += [os.path.join(r, x) for x in f if x.endswith('.xml')]
735 if string.startswith('@string/'):
736 string_search = re.compile(r'.*name="' + string[8:] + '".*?>([^<]+?)<.*').search
737 elif string.startswith('&') and string.endswith(';'):
738 string_search = re.compile(r'.*<!ENTITY.*' + string[1:-1] + '.*?"([^"]+?)".*>').search
740 if string_search is not None:
741 for xmlfile in xmlfiles:
742 for line in file(xmlfile):
743 matches = string_search(line)
745 return retrieve_string(app_dir, matches.group(1), xmlfiles)
748 return string.replace("\\'", "'")
751 # Return list of existing files that will be used to find the highest vercode
752 def manifest_paths(app_dir, flavour):
754 possible_manifests = \
755 [os.path.join(app_dir, 'AndroidManifest.xml'),
756 os.path.join(app_dir, 'src', 'main', 'AndroidManifest.xml'),
757 os.path.join(app_dir, 'src', 'AndroidManifest.xml'),
758 os.path.join(app_dir, 'build.gradle')]
761 possible_manifests.append(
762 os.path.join(app_dir, 'src', flavour, 'AndroidManifest.xml'))
764 return [path for path in possible_manifests if os.path.isfile(path)]
767 # Retrieve the package name. Returns the name, or None if not found.
768 def fetch_real_name(app_dir, flavour):
769 app_search = re.compile(r'.*<application.*').search
770 name_search = re.compile(r'.*android:label="([^"]+)".*').search
772 for f in manifest_paths(app_dir, flavour):
773 if not has_extension(f, 'xml'):
775 logging.debug("fetch_real_name: Checking manifest at " + f)
781 matches = name_search(line)
783 stringname = matches.group(1)
784 logging.debug("fetch_real_name: using string " + stringname)
785 result = retrieve_string(app_dir, stringname)
787 result = result.strip()
792 # Retrieve the version name
793 def version_name(original, app_dir, flavour):
794 for f in manifest_paths(app_dir, flavour):
795 if not has_extension(f, 'xml'):
797 string = retrieve_string(app_dir, original)
803 def get_library_references(root_dir):
805 proppath = os.path.join(root_dir, 'project.properties')
806 if not os.path.isfile(proppath):
808 with open(proppath) as f:
809 for line in f.readlines():
810 if not line.startswith('android.library.reference.'):
812 path = line.split('=')[1].strip()
813 relpath = os.path.join(root_dir, path)
814 if not os.path.isdir(relpath):
816 logging.info("Found subproject at %s" % path)
817 libraries.append(path)
821 def ant_subprojects(root_dir):
822 subprojects = get_library_references(root_dir)
823 for subpath in subprojects:
824 subrelpath = os.path.join(root_dir, subpath)
825 for p in get_library_references(subrelpath):
826 relp = os.path.normpath(os.path.join(subpath, p))
827 if relp not in subprojects:
828 subprojects.insert(0, relp)
832 def remove_debuggable_flags(root_dir):
833 # Remove forced debuggable flags
834 logging.info("Removing debuggable flags")
835 for root, dirs, files in os.walk(root_dir):
836 if 'AndroidManifest.xml' in files:
837 path = os.path.join(root, 'AndroidManifest.xml')
838 p = FDroidPopen(['sed', '-i', 's/android:debuggable="[^"]*"//g', path])
839 if p.returncode != 0:
840 raise BuildException("Failed to remove debuggable flags of %s" % path)
843 # Extract some information from the AndroidManifest.xml at the given path.
844 # Returns (version, vercode, package), any or all of which might be None.
845 # All values returned are strings.
846 def parse_androidmanifests(paths, ignoreversions=None):
849 return (None, None, None)
851 vcsearch = re.compile(r'.*:versionCode="([0-9]+?)".*').search
852 vnsearch = re.compile(r'.*:versionName="([^"]+?)".*').search
853 psearch = re.compile(r'.*package="([^"]+)".*').search
855 vcsearch_g = re.compile(r'.*versionCode *=* *["\']*([0-9]+)["\']*').search
856 vnsearch_g = re.compile(r'.*versionName *=* *(["\'])((?:(?=(\\?))\3.)*?)\1.*').search
857 psearch_g = re.compile(r'.*packageName *=* *["\']([^"]+)["\'].*').search
859 ignoresearch = re.compile(ignoreversions).search if ignoreversions else None
869 gradle = has_extension(path, 'gradle')
870 # Remember package name, may be defined separately from version+vercode
871 package = max_package
873 for line in file(path):
876 matches = psearch_g(line)
878 matches = psearch(line)
880 package = matches.group(1)
883 matches = vnsearch_g(line)
885 matches = vnsearch(line)
887 version = matches.group(2 if gradle else 1)
890 matches = vcsearch_g(line)
892 matches = vcsearch(line)
894 vercode = matches.group(1)
896 # Better some package name than nothing
897 if max_package is None:
898 max_package = package
900 if max_vercode is None or (vercode is not None and vercode > max_vercode):
901 if not ignoresearch or not ignoresearch(version):
902 max_version = version
903 max_vercode = vercode
904 max_package = package
906 max_version = "Ignore"
908 if max_version is None:
909 max_version = version if version else "Unknown"
911 return (max_version, max_vercode, max_package)
914 class BuildException(Exception):
915 def __init__(self, value, detail=None):
919 def get_wikitext(self):
920 ret = repr(self.value) + "\n"
924 txt = self.detail[-8192:] if len(self.detail) > 8192 else self.detail
932 ret += "\n==== detail begin ====\n%s\n==== detail end ====" % self.detail.strip()
936 class VCSException(Exception):
937 def __init__(self, value):
944 # Get the specified source library.
945 # Returns the path to it. Normally this is the path to be used when referencing
946 # it, which may be a subdirectory of the actual project. If you want the base
947 # directory of the project, pass 'basepath=True'.
948 def getsrclib(spec, srclib_dir, srclibpaths=[], subdir=None,
949 basepath=False, raw=False, prepare=True, preponly=False):
957 name, ref = spec.split('@')
959 number, name = name.split(':', 1)
961 name, subdir = name.split('/', 1)
963 if name not in metadata.srclibs:
964 raise BuildException('srclib ' + name + ' not found.')
966 srclib = metadata.srclibs[name]
968 sdir = os.path.join(srclib_dir, name)
971 vcs = getvcs(srclib["Repo Type"], srclib["Repo"], sdir)
972 vcs.srclib = (name, number, sdir)
974 vcs.gotorevision(ref)
981 libdir = os.path.join(sdir, subdir)
982 elif srclib["Subdir"]:
983 for subdir in srclib["Subdir"]:
984 libdir_candidate = os.path.join(sdir, subdir)
985 if os.path.exists(libdir_candidate):
986 libdir = libdir_candidate
992 if srclib["Srclibs"]:
994 for lib in srclib["Srclibs"].replace(';', ',').split(','):
996 for t in srclibpaths:
1001 raise BuildException('Missing recursive srclib %s for %s' % (
1003 place_srclib(libdir, n, s_tuple[2])
1006 remove_signing_keys(sdir)
1007 remove_debuggable_flags(sdir)
1011 if srclib["Prepare"]:
1012 cmd = replace_config_vars(srclib["Prepare"])
1014 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=libdir)
1015 if p.returncode != 0:
1016 raise BuildException("Error running prepare command for srclib %s"
1022 return (name, number, libdir)
1025 # Prepare the source code for a particular build
1026 # 'vcs' - the appropriate vcs object for the application
1027 # 'app' - the application details from the metadata
1028 # 'build' - the build details from the metadata
1029 # 'build_dir' - the path to the build directory, usually
1031 # 'srclib_dir' - the path to the source libraries directory, usually
1033 # 'extlib_dir' - the path to the external libraries directory, usually
1035 # Returns the (root, srclibpaths) where:
1036 # 'root' is the root directory, which may be the same as 'build_dir' or may
1037 # be a subdirectory of it.
1038 # 'srclibpaths' is information on the srclibs being used
1039 def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=False):
1041 # Optionally, the actual app source can be in a subdirectory
1043 root_dir = os.path.join(build_dir, build['subdir'])
1045 root_dir = build_dir
1047 # Get a working copy of the right revision
1048 logging.info("Getting source for revision " + build['commit'])
1049 vcs.gotorevision(build['commit'])
1051 # Initialise submodules if requred
1052 if build['submodules']:
1053 logging.info("Initialising submodules")
1054 vcs.initsubmodules()
1056 # Check that a subdir (if we're using one) exists. This has to happen
1057 # after the checkout, since it might not exist elsewhere
1058 if not os.path.exists(root_dir):
1059 raise BuildException('Missing subdir ' + root_dir)
1061 # Run an init command if one is required
1063 cmd = replace_config_vars(build['init'])
1064 logging.info("Running 'init' commands in %s" % root_dir)
1066 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
1067 if p.returncode != 0:
1068 raise BuildException("Error running init command for %s:%s" %
1069 (app['id'], build['version']), p.stdout)
1071 # Apply patches if any
1073 logging.info("Applying patches")
1074 for patch in build['patch']:
1075 patch = patch.strip()
1076 logging.info("Applying " + patch)
1077 patch_path = os.path.join('metadata', app['id'], patch)
1078 p = FDroidPopen(['patch', '-p1', '-i', os.path.abspath(patch_path)], cwd=build_dir)
1079 if p.returncode != 0:
1080 raise BuildException("Failed to apply patch %s" % patch_path)
1082 # Get required source libraries
1084 if build['srclibs']:
1085 logging.info("Collecting source libraries")
1086 for lib in build['srclibs']:
1087 srclibpaths.append(getsrclib(lib, srclib_dir, srclibpaths,
1090 for name, number, libpath in srclibpaths:
1091 place_srclib(root_dir, int(number) if number else None, libpath)
1093 basesrclib = vcs.getsrclib()
1094 # If one was used for the main source, add that too.
1096 srclibpaths.append(basesrclib)
1098 # Update the local.properties file
1099 localprops = [os.path.join(build_dir, 'local.properties')]
1101 localprops += [os.path.join(root_dir, 'local.properties')]
1102 for path in localprops:
1103 if not os.path.isfile(path):
1105 logging.info("Updating properties file at %s" % path)
1110 # Fix old-fashioned 'sdk-location' by copying
1111 # from sdk.dir, if necessary
1112 if build['oldsdkloc']:
1113 sdkloc = re.match(r".*^sdk.dir=(\S+)$.*", props,
1114 re.S | re.M).group(1)
1115 props += "sdk-location=%s\n" % sdkloc
1117 props += "sdk.dir=%s\n" % config['sdk_path']
1118 props += "sdk-location=%s\n" % config['sdk_path']
1119 if 'ndk_path' in config:
1121 props += "ndk.dir=%s\n" % config['ndk_path']
1122 props += "ndk-location=%s\n" % config['ndk_path']
1123 # Add java.encoding if necessary
1124 if build['encoding']:
1125 props += "java.encoding=%s\n" % build['encoding']
1131 if build['type'] == 'gradle':
1132 flavour = build['gradle'].split('@')[0]
1133 if flavour in ['main', 'yes', '']:
1136 version_regex = re.compile(r".*'com\.android\.tools\.build:gradle:([^\.]+\.[^\.]+).*'.*")
1137 gradlepluginver = None
1139 gradle_files = [os.path.join(root_dir, 'build.gradle')]
1141 # Parent dir build.gradle
1142 parent_dir = os.path.normpath(os.path.join(root_dir, '..'))
1143 if parent_dir.startswith(build_dir):
1144 gradle_files.append(os.path.join(parent_dir, 'build.gradle'))
1146 # Gradle execution dir build.gradle
1147 if '@' in build['gradle']:
1148 gradle_file = os.path.join(root_dir, build['gradle'].split('@', 1)[1], 'build.gradle')
1149 gradle_file = os.path.normpath(gradle_file)
1150 if gradle_file not in gradle_files:
1151 gradle_files.append(gradle_file)
1153 for path in gradle_files:
1156 if not os.path.isfile(path):
1158 with open(path) as f:
1160 match = version_regex.match(line)
1162 gradlepluginver = match.group(1)
1166 build['gradlepluginver'] = LooseVersion(gradlepluginver)
1168 logging.warn("Could not fetch the gradle plugin version, defaulting to 0.11")
1169 build['gradlepluginver'] = LooseVersion('0.11')
1172 n = build["target"].split('-')[1]
1173 FDroidPopen(['sed', '-i',
1174 's@compileSdkVersion *[0-9]*@compileSdkVersion ' + n + '@g',
1177 if '@' in build['gradle']:
1178 gradle_dir = os.path.join(root_dir, build['gradle'].split('@', 1)[1])
1179 gradle_dir = os.path.normpath(gradle_dir)
1180 FDroidPopen(['sed', '-i',
1181 's@compileSdkVersion *[0-9]*@compileSdkVersion ' + n + '@g',
1185 # Remove forced debuggable flags
1186 remove_debuggable_flags(root_dir)
1188 # Insert version code and number into the manifest if necessary
1189 if build['forceversion']:
1190 logging.info("Changing the version name")
1191 for path in manifest_paths(root_dir, flavour):
1192 if not os.path.isfile(path):
1194 if has_extension(path, 'xml'):
1195 p = SilentPopen(['sed', '-i',
1196 's/android:versionName="[^"]*"/android:versionName="'
1197 + build['version'] + '"/g',
1199 if p.returncode != 0:
1200 raise BuildException("Failed to amend manifest")
1201 elif has_extension(path, 'gradle'):
1202 p = SilentPopen(['sed', '-i',
1203 's/versionName *=* *"[^"]*"/versionName = "'
1204 + build['version'] + '"/g',
1206 if p.returncode != 0:
1207 raise BuildException("Failed to amend build.gradle")
1208 if build['forcevercode']:
1209 logging.info("Changing the version code")
1210 for path in manifest_paths(root_dir, flavour):
1211 if not os.path.isfile(path):
1213 if has_extension(path, 'xml'):
1214 p = SilentPopen(['sed', '-i',
1215 's/android:versionCode="[^"]*"/android:versionCode="'
1216 + build['vercode'] + '"/g',
1218 if p.returncode != 0:
1219 raise BuildException("Failed to amend manifest")
1220 elif has_extension(path, 'gradle'):
1221 p = SilentPopen(['sed', '-i',
1222 's/versionCode *=* *[0-9]*/versionCode = '
1223 + build['vercode'] + '/g',
1225 if p.returncode != 0:
1226 raise BuildException("Failed to amend build.gradle")
1228 # Delete unwanted files
1230 logging.info("Removing specified files")
1231 for part in getpaths(build_dir, build, 'rm'):
1232 dest = os.path.join(build_dir, part)
1233 logging.info("Removing {0}".format(part))
1234 if os.path.lexists(dest):
1235 if os.path.islink(dest):
1236 SilentPopen(['unlink ' + dest], shell=True)
1238 SilentPopen(['rm -rf ' + dest], shell=True)
1240 logging.info("...but it didn't exist")
1242 remove_signing_keys(build_dir)
1244 # Add required external libraries
1245 if build['extlibs']:
1246 logging.info("Collecting prebuilt libraries")
1247 libsdir = os.path.join(root_dir, 'libs')
1248 if not os.path.exists(libsdir):
1250 for lib in build['extlibs']:
1252 logging.info("...installing extlib {0}".format(lib))
1253 libf = os.path.basename(lib)
1254 libsrc = os.path.join(extlib_dir, lib)
1255 if not os.path.exists(libsrc):
1256 raise BuildException("Missing extlib file {0}".format(libsrc))
1257 shutil.copyfile(libsrc, os.path.join(libsdir, libf))
1259 # Run a pre-build command if one is required
1260 if build['prebuild']:
1261 logging.info("Running 'prebuild' commands in %s" % root_dir)
1263 cmd = replace_config_vars(build['prebuild'])
1265 # Substitute source library paths into prebuild commands
1266 for name, number, libpath in srclibpaths:
1267 libpath = os.path.relpath(libpath, root_dir)
1268 cmd = cmd.replace('$$' + name + '$$', libpath)
1270 p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
1271 if p.returncode != 0:
1272 raise BuildException("Error running prebuild command for %s:%s" %
1273 (app['id'], build['version']), p.stdout)
1275 # Generate (or update) the ant build file, build.xml...
1276 if build['update'] and build['update'] != ['no'] and build['type'] == 'ant':
1277 parms = [os.path.join(config['sdk_path'], 'tools', 'android'), 'update']
1278 lparms = parms + ['lib-project']
1279 parms = parms + ['project']
1282 parms += ['-t', build['target']]
1283 lparms += ['-t', build['target']]
1284 if build['update'] == ['auto']:
1285 update_dirs = ant_subprojects(root_dir) + ['.']
1287 update_dirs = build['update']
1289 for d in update_dirs:
1290 subdir = os.path.join(root_dir, d)
1292 print("Updating main project")
1293 cmd = parms + ['-p', d]
1295 print("Updating subproject %s" % d)
1296 cmd = lparms + ['-p', d]
1297 p = FDroidPopen(cmd, cwd=root_dir)
1298 # Check to see whether an error was returned without a proper exit
1299 # code (this is the case for the 'no target set or target invalid'
1301 if p.returncode != 0 or p.stdout.startswith("Error: "):
1302 raise BuildException("Failed to update project at %s" % d, p.stdout)
1303 # Clean update dirs via ant
1305 logging.info("Cleaning subproject %s" % d)
1306 p = FDroidPopen(['ant', 'clean'], cwd=subdir)
1308 return (root_dir, srclibpaths)
1311 # Split and extend via globbing the paths from a field
1312 def getpaths(build_dir, build, field):
1314 for p in build[field]:
1316 full_path = os.path.join(build_dir, p)
1317 full_path = os.path.normpath(full_path)
1318 paths += [r[len(build_dir) + 1:] for r in glob.glob(full_path)]
1322 # Scan the source code in the given directory (and all subdirectories)
1323 # and return the number of fatal problems encountered
1324 def scan_source(build_dir, root_dir, thisbuild):
1328 # Common known non-free blobs (always lower case):
1330 re.compile(r'flurryagent', re.IGNORECASE),
1331 re.compile(r'paypal.*mpl', re.IGNORECASE),
1332 re.compile(r'libgoogleanalytics', re.IGNORECASE),
1333 re.compile(r'admob.*sdk.*android', re.IGNORECASE),
1334 re.compile(r'googleadview', re.IGNORECASE),
1335 re.compile(r'googleadmobadssdk', re.IGNORECASE),
1336 re.compile(r'google.*play.*services', re.IGNORECASE),
1337 re.compile(r'crittercism', re.IGNORECASE),
1338 re.compile(r'heyzap', re.IGNORECASE),
1339 re.compile(r'jpct.*ae', re.IGNORECASE),
1340 re.compile(r'youtubeandroidplayerapi', re.IGNORECASE),
1341 re.compile(r'bugsense', re.IGNORECASE),
1342 re.compile(r'crashlytics', re.IGNORECASE),
1343 re.compile(r'ouya.*sdk', re.IGNORECASE),
1344 re.compile(r'libspen23', re.IGNORECASE),
1347 scanignore = getpaths(build_dir, thisbuild, 'scanignore')
1348 scandelete = getpaths(build_dir, thisbuild, 'scandelete')
1351 ms = magic.open(magic.MIME_TYPE)
1353 except AttributeError:
1357 for i in scanignore:
1358 if fd.startswith(i):
1363 for i in scandelete:
1364 if fd.startswith(i):
1368 def removeproblem(what, fd, fp):
1369 logging.info('Removing %s at %s' % (what, fd))
1372 def warnproblem(what, fd):
1373 logging.warn('Found %s at %s' % (what, fd))
1375 def handleproblem(what, fd, fp):
1377 removeproblem(what, fd, fp)
1379 logging.error('Found %s at %s' % (what, fd))
1383 def insidedir(path, dirname):
1384 return path.endswith('/%s' % dirname) or '/%s/' % dirname in path
1386 # Iterate through all files in the source code
1387 for r, d, f in os.walk(build_dir):
1389 if any(insidedir(r, d) for d in ('.hg', '.git', '.svn', '.bzr')):
1394 # Path (relative) to the file
1395 fp = os.path.join(r, curfile)
1396 fd = fp[len(build_dir) + 1:]
1398 # Check if this file has been explicitly excluded from scanning
1402 mime = magic.from_file(fp, mime=True) if ms is None else ms.file(fp)
1404 if mime == 'application/x-sharedlib':
1405 count += handleproblem('shared library', fd, fp)
1407 elif mime == 'application/x-archive':
1408 count += handleproblem('static library', fd, fp)
1410 elif mime == 'application/x-executable':
1411 count += handleproblem('binary executable', fd, fp)
1413 elif mime == 'application/x-java-applet':
1414 count += handleproblem('Java compiled class', fd, fp)
1419 'application/java-archive',
1420 'application/octet-stream',
1424 if has_extension(fp, 'apk'):
1425 removeproblem('APK file', fd, fp)
1427 elif has_extension(fp, 'jar'):
1429 if any(suspect.match(curfile) for suspect in usual_suspects):
1430 count += handleproblem('usual supect', fd, fp)
1432 warnproblem('JAR file', fd)
1434 elif has_extension(fp, 'zip'):
1435 warnproblem('ZIP file', fd)
1438 warnproblem('unknown compressed or binary file', fd)
1440 elif has_extension(fp, 'java'):
1441 for line in file(fp):
1442 if 'DexClassLoader' in line:
1443 count += handleproblem('DexClassLoader', fd, fp)
1448 # Presence of a jni directory without buildjni=yes might
1449 # indicate a problem (if it's not a problem, explicitly use
1450 # buildjni=no to bypass this check)
1451 if (os.path.exists(os.path.join(root_dir, 'jni')) and
1452 not thisbuild['buildjni']):
1453 logging.error('Found jni directory, but buildjni is not enabled')
1462 self.path = os.path.join('stats', 'known_apks.txt')
1464 if os.path.exists(self.path):
1465 for line in file(self.path):
1466 t = line.rstrip().split(' ')
1468 self.apks[t[0]] = (t[1], None)
1470 self.apks[t[0]] = (t[1], time.strptime(t[2], '%Y-%m-%d'))
1471 self.changed = False
1473 def writeifchanged(self):
1475 if not os.path.exists('stats'):
1477 f = open(self.path, 'w')
1479 for apk, app in self.apks.iteritems():
1481 line = apk + ' ' + appid
1483 line += ' ' + time.strftime('%Y-%m-%d', added)
1485 for line in sorted(lst):
1486 f.write(line + '\n')
1489 # Record an apk (if it's new, otherwise does nothing)
1490 # Returns the date it was added.
1491 def recordapk(self, apk, app):
1492 if apk not in self.apks:
1493 self.apks[apk] = (app, time.gmtime(time.time()))
1495 _, added = self.apks[apk]
1498 # Look up information - given the 'apkname', returns (app id, date added/None).
1499 # Or returns None for an unknown apk.
1500 def getapp(self, apkname):
1501 if apkname in self.apks:
1502 return self.apks[apkname]
1505 # Get the most recent 'num' apps added to the repo, as a list of package ids
1506 # with the most recent first.
1507 def getlatest(self, num):
1509 for apk, app in self.apks.iteritems():
1513 if apps[appid] > added:
1517 sortedapps = sorted(apps.iteritems(), key=operator.itemgetter(1))[-num:]
1518 lst = [app for app, _ in sortedapps]
1523 def isApkDebuggable(apkfile, config):
1524 """Returns True if the given apk file is debuggable
1526 :param apkfile: full path to the apk to check"""
1528 p = SilentPopen([os.path.join(config['sdk_path'], 'build-tools',
1529 config['build_tools'], 'aapt'),
1530 'dump', 'xmltree', apkfile, 'AndroidManifest.xml'])
1531 if p.returncode != 0:
1532 logging.critical("Failed to get apk manifest information")
1534 for line in p.stdout.splitlines():
1535 if 'android:debuggable' in line and not line.endswith('0x0'):
1540 class AsynchronousFileReader(threading.Thread):
1542 Helper class to implement asynchronous reading of a file
1543 in a separate thread. Pushes read lines on a queue to
1544 be consumed in another thread.
1547 def __init__(self, fd, queue):
1548 assert isinstance(queue, Queue.Queue)
1549 assert callable(fd.readline)
1550 threading.Thread.__init__(self)
1555 '''The body of the tread: read lines and put them on the queue.'''
1556 for line in iter(self._fd.readline, ''):
1557 self._queue.put(line)
1560 '''Check whether there is no more content to expect.'''
1561 return not self.is_alive() and self._queue.empty()
1569 def SilentPopen(commands, cwd=None, shell=False):
1570 return FDroidPopen(commands, cwd=cwd, shell=shell, output=False)
1573 def FDroidPopen(commands, cwd=None, shell=False, output=False):
1575 Run a command and capture the possibly huge output.
1577 :param commands: command and argument list like in subprocess.Popen
1578 :param cwd: optionally specifies a working directory
1579 :returns: A PopenResult.
1584 cwd = os.path.normpath(cwd)
1585 logging.info("Directory: %s" % cwd)
1586 logging.info("> %s" % ' '.join(commands))
1588 result = PopenResult()
1589 p = subprocess.Popen(commands, cwd=cwd, shell=shell,
1590 stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
1592 stdout_queue = Queue.Queue()
1593 stdout_reader = AsynchronousFileReader(p.stdout, stdout_queue)
1594 stdout_reader.start()
1596 # Check the queue for output (until there is no more to get)
1597 while not stdout_reader.eof():
1598 while not stdout_queue.empty():
1599 line = stdout_queue.get()
1600 if output or options.verbose:
1601 # Output directly to console
1602 sys.stdout.write(line)
1604 result.stdout += line
1609 result.returncode = p.returncode
1613 def remove_signing_keys(build_dir):
1614 comment = re.compile(r'[ ]*//')
1615 signing_configs = re.compile(r'^[\t ]*signingConfigs[ \t]*{[ \t]*$')
1617 re.compile(r'^[\t ]*signingConfig [^ ]*$'),
1618 re.compile(r'.*android\.signingConfigs\.[^{]*$'),
1619 re.compile(r'.*variant\.outputFile = .*'),
1620 re.compile(r'.*\.readLine\(.*'),
1622 for root, dirs, files in os.walk(build_dir):
1623 if 'build.gradle' in files:
1624 path = os.path.join(root, 'build.gradle')
1626 with open(path, "r") as o:
1627 lines = o.readlines()
1630 with open(path, "w") as o:
1632 if comment.match(line):
1636 opened += line.count('{')
1637 opened -= line.count('}')
1640 if signing_configs.match(line):
1644 if any(s.match(line) for s in line_matches):
1650 logging.info("Cleaned build.gradle of keysigning configs at %s" % path)
1653 'project.properties',
1655 'default.properties',
1658 if propfile in files:
1659 path = os.path.join(root, propfile)
1661 with open(path, "r") as o:
1662 lines = o.readlines()
1664 with open(path, "w") as o:
1666 if line.startswith('key.store'):
1668 if line.startswith('key.alias'):
1672 logging.info("Cleaned %s of keysigning configs at %s" % (propfile, path))
1675 def replace_config_vars(cmd):
1676 cmd = cmd.replace('$$SDK$$', config['sdk_path'])
1677 cmd = cmd.replace('$$NDK$$', config['ndk_path'])
1678 cmd = cmd.replace('$$MVN3$$', config['mvn3'])
1682 def place_srclib(root_dir, number, libpath):
1685 relpath = os.path.relpath(libpath, root_dir)
1686 proppath = os.path.join(root_dir, 'project.properties')
1689 if os.path.isfile(proppath):
1690 with open(proppath, "r") as o:
1691 lines = o.readlines()
1693 with open(proppath, "w") as o:
1696 if line.startswith('android.library.reference.%d=' % number):
1697 o.write('android.library.reference.%d=%s\n' % (number, relpath))
1702 o.write('android.library.reference.%d=%s\n' % (number, relpath))