# -*- coding: utf-8 -*-
#
# common.py - part of the FDroid server tools
# Copyright (C) 2010-13, Ciaran Gultnieks, ciaran@ciarang.com
# Copyright (C) 2013 Daniel MartÃ
'
self.state = self.stPARA
elif self.state == self.stPARA:
self.text_html += ' '
self.text_plain += ' '
self.addtext(line)
def end(self):
self.endcur()
# Parse multiple lines of description as written in a metadata file, returning
# a single string in plain text format.
def description_plain(lines, linkres):
ps = DescriptionFormatter(linkres)
for line in lines:
ps.parseline(line)
ps.end()
return ps.text_plain
# Parse multiple lines of description as written in a metadata file, returning
# a single string in wiki format.
def description_wiki(lines):
ps = DescriptionFormatter(None)
for line in lines:
ps.parseline(line)
ps.end()
return ps.text_wiki
# Parse multiple lines of description as written in a metadata file, returning
# a single string in HTML format.
def description_html(lines,linkres):
ps = DescriptionFormatter(linkres)
for line in lines:
ps.parseline(line)
ps.end()
return ps.text_html
def retrieve_string(xml_dir, string):
if not string.startswith('@string/'):
return string.replace("\\'","'")
string_search = re.compile(r'.*"'+string[8:]+'".*>([^<]+?)<.*').search
for xmlfile in glob.glob(os.path.join(xml_dir, '*.xml')):
for line in file(xmlfile):
matches = string_search(line)
if matches:
return retrieve_string(xml_dir, matches.group(1))
return ''
# Return list of existing files that will be used to find the highest vercode
def manifest_paths(app_dir, flavour):
possible_manifests = [ os.path.join(app_dir, 'AndroidManifest.xml'),
os.path.join(app_dir, 'src', 'main', 'AndroidManifest.xml'),
os.path.join(app_dir, 'build.gradle') ]
if flavour is not None:
possible_manifests.append(
os.path.join(app_dir, 'src', flavour, 'AndroidManifest.xml'))
return [path for path in possible_manifests if os.path.isfile(path)]
# Retrieve the package name
def fetch_real_name(app_dir, flavour):
app_search = re.compile(r'.*\n"
ret += str(self.stdout)
ret += "\n"
if self.stderr:
ret += "=stderr=\n"
ret += "\n"
ret += str(self.stderr)
ret += "\n"
return ret
def __str__(self):
ret = repr(self.value)
if self.stdout:
ret += "\n==== stdout begin ====\n%s\n==== stdout end ====" % self.stdout.strip()
if self.stderr:
ret += "\n==== stderr begin ====\n%s\n==== stderr end ====" % self.stderr.strip()
return ret
class VCSException(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)
class MetaDataException(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)
def parse_srclib(metafile, **kw):
thisinfo = {}
if metafile and not isinstance(metafile, file):
metafile = open(metafile, "r")
# Defaults for fields that come from metadata
thisinfo['Repo Type'] = ''
thisinfo['Repo'] = ''
thisinfo['Subdir'] = None
thisinfo['Prepare'] = None
thisinfo['Update Project'] = None
if metafile is None:
return thisinfo
for line in metafile:
line = line.rstrip('\r\n')
if len(line) == 0:
continue
if line.startswith("#"):
continue
index = line.find(':')
if index == -1:
raise MetaDataException("Invalid metadata in " + metafile.name + " at: " + line)
field = line[:index]
value = line[index+1:]
if field == "Subdir":
thisinfo[field] = value.split(',')
else:
thisinfo[field] = value
return thisinfo
# Get the specified source library.
# Returns the path to it. Normally this is the path to be used when referencing
# it, which may be a subdirectory of the actual project. If you want the base
# directory of the project, pass 'basepath=True'.
def getsrclib(spec, srclib_dir, sdk_path, ndk_path="", mvn3="", basepath=False, raw=False, prepare=True, preponly=False):
if raw:
name = spec
ref = None
else:
name, ref = spec.split('@')
srclib_path = os.path.join('srclibs', name + ".txt")
if not os.path.exists(srclib_path):
raise BuildException('srclib ' + name + ' not found.')
srclib = parse_srclib(srclib_path)
sdir = os.path.join(srclib_dir, name)
if not preponly:
vcs = getvcs(srclib["Repo Type"], srclib["Repo"], sdir, sdk_path)
vcs.srclib = (name, sdir)
if ref:
vcs.gotorevision(ref)
if raw:
return vcs
libdir = None
if srclib["Subdir"] is not None:
for subdir in srclib["Subdir"]:
libdir_candidate = os.path.join(sdir, subdir)
if os.path.exists(libdir_candidate):
libdir = libdir_candidate
break
if libdir is None:
libdir = sdir
if prepare:
if srclib["Prepare"] is not None:
cmd = srclib["Prepare"].replace('$$SDK$$', sdk_path)
cmd = cmd.replace('$$NDK$$', ndk_path).replace('$$MVN$$', mvn3)
p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=libdir)
if p.returncode != 0:
raise BuildException("Error running prepare command for srclib %s"
% name, p.stdout, p.stderr)
if srclib["Update Project"] == "Yes":
print "Updating srclib %s at path %s" % (name, libdir)
if subprocess.call([os.path.join(sdk_path, 'tools', 'android'),
'update', 'project', '-p', libdir]) != 0:
raise BuildException( 'Error updating ' + name + ' project')
if basepath:
return sdir
return libdir
# Prepare the source code for a particular build
# 'vcs' - the appropriate vcs object for the application
# 'app' - the application details from the metadata
# 'build' - the build details from the metadata
# 'build_dir' - the path to the build directory, usually
# 'build/app.id'
# 'srclib_dir' - the path to the source libraries directory, usually
# 'build/srclib'
# 'extlib_dir' - the path to the external libraries directory, usually
# 'build/extlib'
# 'sdk_path' - the path to the Android SDK
# 'ndk_path' - the path to the Android NDK
# 'javacc_path' - the path to javacc
# 'mvn3' - the path to the maven 3 executable
# Returns the (root, srclibpaths) where:
# 'root' is the root directory, which may be the same as 'build_dir' or may
# be a subdirectory of it.
# 'srclibpaths' is information on the srclibs being used
def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, sdk_path, ndk_path, javacc_path, mvn3, onserver=False):
# Optionally, the actual app source can be in a subdirectory...
if 'subdir' in build:
root_dir = os.path.join(build_dir, build['subdir'])
else:
root_dir = build_dir
# Get a working copy of the right revision...
print "Getting source for revision " + build['commit']
vcs.gotorevision(build['commit'])
# Check that a subdir (if we're using one) exists. This has to happen
# after the checkout, since it might not exist elsewhere...
if not os.path.exists(root_dir):
raise BuildException('Missing subdir ' + root_dir)
# Initialise submodules if requred...
if build.get('submodules', 'no') == 'yes':
if options.verbose:
print "Initialising submodules..."
vcs.initsubmodules()
# Run an init command if one is required...
if 'init' in build:
cmd = build['init']
cmd = cmd.replace('$$SDK$$', sdk_path)
cmd = cmd.replace('$$NDK$$', ndk_path)
cmd = cmd.replace('$$MVN$$', mvn3)
if options.verbose:
print "Running 'init' commands in %s" % root_dir
p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
if p.returncode != 0:
raise BuildException("Error running init command for %s:%s" %
(app['id'], build['version']), p.stdout, p.stderr)
# Generate (or update) the ant build file, build.xml...
updatemode = build.get('update', 'auto')
if (updatemode != 'no'
and build.get('maven', 'no') == 'no'
and build.get('gradle', 'no') == 'no'):
parms = [os.path.join(sdk_path, 'tools', 'android'),
'update', 'project']
if 'target' in build and build['target']:
parms += ['-t', build['target']]
update_dirs = None
if updatemode == 'auto':
update_dirs = ['.'] + ant_subprojects(root_dir)
else:
update_dirs = [d.strip() for d in updatemode.split(';')]
# Force build.xml update if necessary...
if updatemode == 'force' or 'target' in build:
if updatemode == 'force':
update_dirs = ['.']
buildxml = os.path.join(root_dir, 'build.xml')
if os.path.exists(buildxml):
print 'Force-removing old build.xml'
os.remove(buildxml)
for d in update_dirs:
# Remove gen and bin dirs in libraries
# rid of them...
for baddir in [
'gen', 'bin', 'obj', # ant
'libs/armeabi-v7a', 'libs/armeabi', # jni
'libs/mips', 'libs/x86']:
badpath = os.path.join(root_dir, d, baddir)
if os.path.exists(badpath):
print "Removing '%s'" % badpath
shutil.rmtree(badpath)
dparms = parms + ['-p', d]
if options.verbose:
if d == '.':
print "Updating main project..."
else:
print "Updating subproject %s..." % d
p = FDroidPopen(dparms, cwd=root_dir)
# check to see whether an error was returned without a proper exit code (this is the case for the 'no target set or target invalid' error)
if p.returncode != 0 or (p.stderr != "" and p.stderr.startswith("Error: ")):
raise BuildException("Failed to update project at %s" % d,
p.stdout, p.stderr)
# If the app has ant set up to sign the release, we need to switch
# that off, because we want the unsigned apk...
for propfile in ('build.properties', 'default.properties', 'ant.properties'):
if os.path.exists(os.path.join(root_dir, propfile)):
if subprocess.call(['sed','-i','s/^key.store/#/',
propfile], cwd=root_dir) !=0:
raise BuildException("Failed to amend %s" % propfile)
for root, dirs, files in os.walk(build_dir):
for f in files:
if f == 'build.gradle':
clean_gradle_keys(os.path.join(root, f))
break
# Update the local.properties file...
localprops = [ os.path.join(build_dir, 'local.properties') ]
if 'subdir' in build:
localprops += [ os.path.join(root_dir, 'local.properties') ]
for path in localprops:
if not os.path.isfile(path):
continue
if options.verbose:
print "Updating properties file at %s" % path
f = open(path, 'r')
props = f.read()
f.close()
props += '\n'
# Fix old-fashioned 'sdk-location' by copying
# from sdk.dir, if necessary...
if build.get('oldsdkloc', 'no') == "yes":
sdkloc = re.match(r".*^sdk.dir=(\S+)$.*", props,
re.S|re.M).group(1)
props += "sdk-location=%s\n" % sdkloc
else:
props += "sdk.dir=%s\n" % sdk_path
props += "sdk-location=%s\n" % sdk_path
# Add ndk location...
props += "ndk.dir=%s\n" % ndk_path
props += "ndk-location=%s\n" % ndk_path
# Add java.encoding if necessary...
if 'encoding' in build:
props += "java.encoding=%s\n" % build['encoding']
f = open(path, 'w')
f.write(props)
f.close()
flavour = None
if 'gradle' in build:
flavour = build['gradle'].split('@')[0]
if flavour in ['main', 'yes', '']:
flavour = None
# Remove forced debuggable flags
print "Removing debuggable flags..."
for path in manifest_paths(root_dir, flavour):
if not os.path.isfile(path):
continue
if subprocess.call(['sed','-i',
's/android:debuggable="[^"]*"//g', path]) != 0:
raise BuildException("Failed to remove debuggable flags")
# Insert version code and number into the manifest if necessary...
if 'forceversion' in build:
print "Changing the version name..."
for path in manifest_paths(root_dir, flavour):
if not os.path.isfile(path):
continue
if path.endswith('.xml'):
if subprocess.call(['sed','-i',
's/android:versionName="[^"]*"/android:versionName="' + build['version'] + '"/g',
path]) != 0:
raise BuildException("Failed to amend manifest")
elif path.endswith('.gradle'):
if subprocess.call(['sed','-i',
's/versionName[ ]*=[ ]*"[^"]*"/versionName = "' + build['version'] + '"/g',
path]) != 0:
raise BuildException("Failed to amend build.gradle")
if 'forcevercode' in build:
print "Changing the version code..."
for path in manifest_paths(root_dir, flavour):
if not os.path.isfile(path):
continue
if path.endswith('.xml'):
if subprocess.call(['sed','-i',
's/android:versionCode="[^"]*"/android:versionCode="' + build['vercode'] + '"/g',
path]) != 0:
raise BuildException("Failed to amend manifest")
elif path.endswith('.gradle'):
if subprocess.call(['sed','-i',
's/versionCode[ ]*=[ ]*[0-9]*/versionCode = ' + build['vercode'] + '/g',
path]) != 0:
raise BuildException("Failed to amend build.gradle")
# Delete unwanted files...
if 'rm' in build:
for part in build['rm'].split(';'):
dest = os.path.join(build_dir, part.strip())
if not os.path.realpath(dest).startswith(os.path.realpath(build_dir)):
raise BuildException("rm for {0} is outside build root {1}".format(
os.path.realpath(build_dir),os.path.realpath(dest)))
if os.path.exists(dest):
subprocess.call('rm -rf ' + dest, shell=True)
# Fix apostrophes translation files if necessary...
if build.get('fixapos', 'no') == 'yes':
for root, dirs, files in os.walk(os.path.join(root_dir, 'res')):
for filename in files:
if filename.endswith('.xml'):
if subprocess.call(['sed','-i','s@' +
r"\([^\\]\)'@\1\\'" +
'@g',
os.path.join(root, filename)]) != 0:
raise BuildException("Failed to amend " + filename)
# Fix translation files if necessary...
if build.get('fixtrans', 'no') == 'yes':
for root, dirs, files in os.walk(os.path.join(root_dir, 'res')):
for filename in files:
if filename.endswith('.xml'):
f = open(os.path.join(root, filename))
changed = False
outlines = []
for line in f:
num = 1
index = 0
oldline = line
while True:
index = line.find("%", index)
if index == -1:
break
next = line[index+1:index+2]
if next == "s" or next == "d":
line = (line[:index+1] +
str(num) + "$" +
line[index+1:])
num += 1
index += 3
else:
index += 1
# We only want to insert the positional arguments
# when there is more than one argument...
if oldline != line:
if num > 2:
changed = True
else:
line = oldline
outlines.append(line)
f.close()
if changed:
f = open(os.path.join(root, filename), 'w')
f.writelines(outlines)
f.close()
# Add required external libraries...
if 'extlibs' in build:
print "Collecting prebuilt libraries..."
libsdir = os.path.join(root_dir, 'libs')
if not os.path.exists(libsdir):
os.mkdir(libsdir)
for lib in build['extlibs'].split(';'):
lib = lib.strip()
libf = os.path.basename(lib)
shutil.copyfile(os.path.join(extlib_dir, lib),
os.path.join(libsdir, libf))
# Get required source libraries...
srclibpaths = []
if 'srclibs' in build:
print "Collecting source libraries..."
for lib in build['srclibs'].split(';'):
lib = lib.strip()
name, _ = lib.split('@')
srclibpaths.append((name, getsrclib(lib, srclib_dir, sdk_path, ndk_path, mvn3, preponly=onserver)))
basesrclib = vcs.getsrclib()
# If one was used for the main source, add that too.
if basesrclib:
srclibpaths.append(basesrclib)
# Apply patches if any
if 'patch' in build:
for patch in build['patch'].split(';'):
patch = patch.strip()
print "Applying " + patch
patch_path = os.path.join('metadata', app['id'], patch)
if subprocess.call(['patch', '-p1',
'-i', os.path.abspath(patch_path)], cwd=build_dir) != 0:
raise BuildException("Failed to apply patch %s" % patch_path)
# Run a pre-build command if one is required...
if 'prebuild' in build:
cmd = build['prebuild']
# Substitute source library paths into prebuild commands...
for name, libpath in srclibpaths:
libpath = os.path.relpath(libpath, root_dir)
cmd = cmd.replace('$$' + name + '$$', libpath)
cmd = cmd.replace('$$SDK$$', sdk_path)
cmd = cmd.replace('$$NDK$$', ndk_path)
cmd = cmd.replace('$$MVN3$$', mvn3)
if options.verbose:
print "Running 'prebuild' commands in %s" % root_dir
p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
if p.returncode != 0:
raise BuildException("Error running prebuild command for %s:%s" %
(app['id'], build['version']), p.stdout, p.stderr)
print "Applying generic clean-ups..."
if build.get('anal-tics', 'no') == 'yes':
fp = os.path.join(root_dir, 'src', 'com', 'google', 'android', 'apps', 'analytics')
os.makedirs(fp)
with open(os.path.join(fp, 'GoogleAnalyticsTracker.java'), 'w') as f:
f.write("""
package com.google.android.apps.analytics;
public class GoogleAnalyticsTracker {
private static GoogleAnalyticsTracker instance;
private GoogleAnalyticsTracker() {
}
public static GoogleAnalyticsTracker getInstance() {
if(instance == null)
instance = new GoogleAnalyticsTracker();
return instance;
}
public void start(String i,int think ,Object not) {
}
public void dispatch() {
}
public void stop() {
}
public void setProductVersion(String uh, String hu) {
}
public void trackEvent(String that,String just,String aint,int happening) {
}
public void trackPageView(String nope) {
}
public void setCustomVar(int mind,String your,String own,int business) {
}
}
""")
return (root_dir, srclibpaths)
# Scan the source code in the given directory (and all subdirectories)
# and return a list of potential problems.
def scan_source(build_dir, root_dir, thisbuild):
problems = []
# Common known non-free blobs (always lower case):
usual_suspects = ['flurryagent',
'paypal_mpl',
'libgoogleanalytics',
'admob-sdk-android',
'googleadview',
'googleadmobadssdk',
'google-play-services',
'crittercism',
'heyzap',
'jpct-ae',
'youtubeandroidplayerapi',
'bugsense']
def getpaths(field):
paths = []
if field not in thisbuild:
return paths
for p in thisbuild[field].split(';'):
p = p.strip()
if p == '.':
p = '/'
elif p.startswith('./'):
p = p[1:]
elif not p.startswith('/'):
p = '/' + p;
if p not in paths:
paths.append(p)
return paths
scanignore = getpaths('scanignore')
scandelete = getpaths('scandelete')
ms = magic.open(magic.MIME_TYPE)
ms.load()
def toignore(fd):
for i in scanignore:
if fd.startswith(i):
return True
return False
def todelete(fd):
for i in scandelete:
if fd.startswith(i):
return True
return False
def removeproblem(what, fd, fp):
print 'Removing %s at %s' % (what, fd)
os.remove(fp)
def handleproblem(what, fd, fp):
if todelete(fd):
removeproblem(what, fd, fp)
else:
problems.append('Found %s at %s' % (what, fd))
# Iterate through all files in the source code...
for r,d,f in os.walk(build_dir):
for curfile in f:
if '/.hg' in r or '/.git' in r or '/.svn' in r:
continue
# Path (relative) to the file...
fp = os.path.join(r, curfile)
fd = fp[len(build_dir):]
# Check if this file has been explicitly excluded from scanning...
if toignore(fd):
continue
for suspect in usual_suspects:
if suspect in curfile.lower():
handleproblem('usual supect', fd, fp)
mime = ms.file(fp)
if mime == 'application/x-sharedlib':
handleproblem('shared library', fd, fp)
elif mime == 'application/x-archive':
handleproblem('static library', fd, fp)
elif mime == 'application/x-executable':
handleproblem('binary executable', fd, fp)
elif mime == 'application/jar' and fp.endswith('.apk'):
removeproblem('APK file', fd, fp)
elif curfile.endswith('.java'):
for line in file(fp):
if 'DexClassLoader' in line:
handleproblem('DexClassLoader', fd, fp)
break
ms.close()
# Presence of a jni directory without buildjni=yes might
# indicate a problem... (if it's not a problem, explicitly use
# buildjni=no to bypass this check)
if (os.path.exists(os.path.join(root_dir, 'jni')) and
thisbuild.get('buildjni') is None):
msg = 'Found jni directory, but buildjni is not enabled'
problems.append(msg)
return problems
class KnownApks:
def __init__(self):
self.path = os.path.join('stats', 'known_apks.txt')
self.apks = {}
if os.path.exists(self.path):
for line in file( self.path):
t = line.rstrip().split(' ')
if len(t) == 2:
self.apks[t[0]] = (t[1], None)
else:
self.apks[t[0]] = (t[1], time.strptime(t[2], '%Y-%m-%d'))
self.changed = False
def writeifchanged(self):
if self.changed:
if not os.path.exists('stats'):
os.mkdir('stats')
f = open(self.path, 'w')
lst = []
for apk, app in self.apks.iteritems():
appid, added = app
line = apk + ' ' + appid
if added:
line += ' ' + time.strftime('%Y-%m-%d', added)
lst.append(line)
for line in sorted(lst):
f.write(line + '\n')
f.close()
# Record an apk (if it's new, otherwise does nothing)
# Returns the date it was added.
def recordapk(self, apk, app):
if not apk in self.apks:
self.apks[apk] = (app, time.gmtime(time.time()))
self.changed = True
_, added = self.apks[apk]
return added
# Look up information - given the 'apkname', returns (app id, date added/None).
# Or returns None for an unknown apk.
def getapp(self, apkname):
if apkname in self.apks:
return self.apks[apkname]
return None
# Get the most recent 'num' apps added to the repo, as a list of package ids
# with the most recent first.
def getlatest(self, num):
apps = {}
for apk, app in self.apks.iteritems():
appid, added = app
if added:
if appid in apps:
if apps[appid] > added:
apps[appid] = added
else:
apps[appid] = added
sortedapps = sorted(apps.iteritems(), key=operator.itemgetter(1))[-num:]
lst = [app for app,added in sortedapps]
lst.reverse()
return lst
def isApkDebuggable(apkfile, config):
"""Returns True if the given apk file is debuggable
:param apkfile: full path to the apk to check"""
p = subprocess.Popen([os.path.join(config['sdk_path'],
'build-tools', config['build_tools'], 'aapt'),
'dump', 'xmltree', apkfile, 'AndroidManifest.xml'],
stdout=subprocess.PIPE)
output = p.communicate()[0]
if p.returncode != 0:
print "ERROR: Failed to get apk manifest information"
sys.exit(1)
for line in output.splitlines():
if line.find('android:debuggable') != -1 and not line.endswith('0x0'):
return True
return False
class AsynchronousFileReader(threading.Thread):
'''
Helper class to implement asynchronous reading of a file
in a separate thread. Pushes read lines on a queue to
be consumed in another thread.
'''
def __init__(self, fd, queue):
assert isinstance(queue, Queue.Queue)
assert callable(fd.readline)
threading.Thread.__init__(self)
self._fd = fd
self._queue = queue
def run(self):
'''The body of the tread: read lines and put them on the queue.'''
for line in iter(self._fd.readline, ''):
self._queue.put(line)
def eof(self):
'''Check whether there is no more content to expect.'''
return not self.is_alive() and self._queue.empty()
class PopenResult:
returncode = None
stdout = ''
stderr = ''
stdout_apk = ''
def FDroidPopen(commands, cwd):
"""
Runs a command the FDroid way and returns return code and output
:param commands, cwd: like subprocess.Popen
"""
if options.verbose:
print "Directory: %s" % cwd
print " > %s" % ' '.join(commands)
result = PopenResult()
p = subprocess.Popen(commands, cwd=cwd,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout_queue = Queue.Queue()
stdout_reader = AsynchronousFileReader(p.stdout, stdout_queue)
stdout_reader.start()
stderr_queue = Queue.Queue()
stderr_reader = AsynchronousFileReader(p.stderr, stderr_queue)
stderr_reader.start()
# Check the queues for output (until there is no more to get)
while not stdout_reader.eof() or not stderr_reader.eof():
# Show what we received from standard output
while not stdout_queue.empty():
line = stdout_queue.get()
if options.verbose:
# Output directly to console
sys.stdout.write(line)
sys.stdout.flush()
result.stdout += line
# Show what we received from standard error
while not stderr_queue.empty():
line = stderr_queue.get()
if options.verbose:
# Output directly to console
sys.stderr.write(line)
sys.stderr.flush()
result.stderr += line
time.sleep(0.2)
p.communicate()
result.returncode = p.returncode
return result
def clean_gradle_keys(path):
if options.verbose:
print "Cleaning build.gradle of keysigning configs at %s" % path
lines = None
with open(path, "r") as o:
lines = o.readlines()
opened = 0
with open(path, "w") as o:
for line in lines:
if 'signingConfigs ' in line:
opened = 1
elif opened > 0:
if '{' in line:
opened += 1
elif '}' in line:
opened -=1
elif not any(s in line for s in (' signingConfig ',)):
o.write(line)