env = None
-def get_default_config():
- return {
- 'sdk_path': os.getenv("ANDROID_HOME") or "",
- 'ndk_path': os.getenv("ANDROID_NDK") or "",
- 'build_tools': "20.0.0",
- 'ant': "ant",
- 'mvn3': "mvn",
- 'gradle': 'gradle',
- 'sync_from_local_copy_dir': False,
- 'update_stats': False,
- 'stats_to_carbon': False,
- 'repo_maxage': 0,
- 'build_server_always': False,
- 'keystore': os.path.join(os.getenv("HOME"), '.local', 'share', 'fdroidserver', 'keystore.jks'),
- 'smartcardoptions': [],
- 'char_limits': {
- 'Summary': 50,
- 'Description': 1500
- },
- 'keyaliases': {},
- 'repo_url': "https://MyFirstFDroidRepo.org/fdroid/repo",
- 'repo_name': "My First FDroid Repo Demo",
- 'repo_icon': "fdroid-icon.png",
- 'repo_description': '''
- This is a repository of apps to be used with FDroid. Applications in this
- repository are either official binaries built by the original application
- developers, or are binaries built from source by the admin of f-droid.org
- using the tools on https://gitlab.com/u/fdroid.
- ''',
- 'archive_older': 0,
- }
+default_config = {
+ 'sdk_path': "$ANDROID_HOME",
+ 'ndk_path': "$ANDROID_NDK",
+ 'build_tools': "21.1.2",
+ 'ant': "ant",
+ 'mvn3': "mvn",
+ 'gradle': 'gradle',
+ 'sync_from_local_copy_dir': False,
+ 'make_current_version_link': True,
+ 'current_version_name_source': 'Name',
+ 'update_stats': False,
+ 'stats_ignore': [],
+ 'stats_server': None,
+ 'stats_user': None,
+ 'stats_to_carbon': False,
+ 'repo_maxage': 0,
+ 'build_server_always': False,
+ 'keystore': os.path.join("$HOME", '.local', 'share', 'fdroidserver', 'keystore.jks'),
+ 'smartcardoptions': [],
+ 'char_limits': {
+ 'Summary': 50,
+ 'Description': 1500
+ },
+ 'keyaliases': {},
+ 'repo_url': "https://MyFirstFDroidRepo.org/fdroid/repo",
+ 'repo_name': "My First FDroid Repo Demo",
+ 'repo_icon': "fdroid-icon.png",
+ 'repo_description': '''
+ This is a repository of apps to be used with FDroid. Applications in this
+ repository are either official binaries built by the original application
+ developers, or are binaries built from source by the admin of f-droid.org
+ using the tools on https://gitlab.com/u/fdroid.
+ ''',
+ 'archive_older': 0,
+}
+
+
+def fill_config_defaults(thisconfig):
+ for k, v in default_config.items():
+ if k not in thisconfig:
+ thisconfig[k] = v
+
+ # Expand paths (~users and $vars)
+ for k in ['sdk_path', 'ndk_path', 'ant', 'mvn3', 'gradle', 'keystore', 'repo_icon']:
+ v = thisconfig[k]
+ orig = v
+ v = os.path.expanduser(v)
+ v = os.path.expandvars(v)
+ if orig != v:
+ thisconfig[k] = v
+ thisconfig[k + '_orig'] = orig
def read_config(opts, config_file='config.py'):
if st.st_mode & stat.S_IRWXG or st.st_mode & stat.S_IRWXO:
logging.warn("unsafe permissions on {0} (should be 0600)!".format(config_file))
- defconfig = get_default_config()
- for k, v in defconfig.items():
- if k not in config:
- config[k] = v
-
- # Expand environment variables
- for k, v in config.items():
- if type(v) != str:
- continue
- v = os.path.expanduser(v)
- config[k] = os.path.expandvars(v)
-
- if not test_sdk_exists(config):
- sys.exit(3)
-
- if not test_build_tools_exists(config):
- sys.exit(3)
-
- bin_paths = {
- 'aapt': [
- os.path.join(config['sdk_path'], 'build-tools', config['build_tools'], 'aapt'),
- ],
- 'zipalign': [
- os.path.join(config['sdk_path'], 'tools', 'zipalign'),
- os.path.join(config['sdk_path'], 'build-tools', config['build_tools'], 'zipalign'),
- ],
- 'android': [
- os.path.join(config['sdk_path'], 'tools', 'android'),
- ],
- 'adb': [
- os.path.join(config['sdk_path'], 'platform-tools', 'adb'),
- ],
- }
-
- for b, paths in bin_paths.items():
- config[b] = None
- for path in paths:
- if os.path.isfile(path):
- config[b] = path
- break
- if config[b] is None:
- logging.warn("Could not find %s in any of the following paths:\n%s" % (
- b, '\n'.join(paths)))
+ fill_config_defaults(config)
# There is no standard, so just set up the most common environment
# variables
return config
-def test_sdk_exists(c):
- if c['sdk_path'] is None:
- # c['sdk_path'] is set to the value of ANDROID_HOME by default
- logging.error('No Android SDK found! ANDROID_HOME is not set and sdk_path is not in config.py!')
+def find_sdk_tools_cmd(cmd):
+ '''find a working path to a tool from the Android SDK'''
+
+ tooldirs = []
+ if config is not None and 'sdk_path' in config and os.path.exists(config['sdk_path']):
+ # try to find a working path to this command, in all the recent possible paths
+ if 'build_tools' in config:
+ build_tools = os.path.join(config['sdk_path'], 'build-tools')
+ # if 'build_tools' was manually set and exists, check only that one
+ configed_build_tools = os.path.join(build_tools, config['build_tools'])
+ if os.path.exists(configed_build_tools):
+ tooldirs.append(configed_build_tools)
+ else:
+ # no configed version, so hunt known paths for it
+ for f in sorted(os.listdir(build_tools), reverse=True):
+ if os.path.isdir(os.path.join(build_tools, f)):
+ tooldirs.append(os.path.join(build_tools, f))
+ tooldirs.append(build_tools)
+ sdk_tools = os.path.join(config['sdk_path'], 'tools')
+ if os.path.exists(sdk_tools):
+ tooldirs.append(sdk_tools)
+ sdk_platform_tools = os.path.join(config['sdk_path'], 'platform-tools')
+ if os.path.exists(sdk_platform_tools):
+ tooldirs.append(sdk_platform_tools)
+ tooldirs.append('/usr/bin')
+ for d in tooldirs:
+ if os.path.isfile(os.path.join(d, cmd)):
+ return os.path.join(d, cmd)
+ # did not find the command, exit with error message
+ ensure_build_tools_exists(config)
+
+
+def test_sdk_exists(thisconfig):
+ if 'sdk_path' not in thisconfig:
+ if 'aapt' in thisconfig and os.path.isfile(thisconfig['aapt']):
+ return True
+ else:
+ logging.error("'sdk_path' not set in config.py!")
+ return False
+ if thisconfig['sdk_path'] == default_config['sdk_path']:
+ logging.error('No Android SDK found!')
logging.error('You can use ANDROID_HOME to set the path to your SDK, i.e.:')
logging.error('\texport ANDROID_HOME=/opt/android-sdk')
return False
- if not os.path.exists(c['sdk_path']):
- logging.critical('Android SDK path "' + c['sdk_path'] + '" does not exist!')
+ if not os.path.exists(thisconfig['sdk_path']):
+ logging.critical('Android SDK path "' + thisconfig['sdk_path'] + '" does not exist!')
return False
- if not os.path.isdir(c['sdk_path']):
- logging.critical('Android SDK path "' + c['sdk_path'] + '" is not a directory!')
+ if not os.path.isdir(thisconfig['sdk_path']):
+ logging.critical('Android SDK path "' + thisconfig['sdk_path'] + '" is not a directory!')
return False
for d in ['build-tools', 'platform-tools', 'tools']:
- if not os.path.isdir(os.path.join(c['sdk_path'], d)):
+ if not os.path.isdir(os.path.join(thisconfig['sdk_path'], d)):
logging.critical('Android SDK path "%s" does not contain "%s/"!' % (
- c['sdk_path'], d))
+ thisconfig['sdk_path'], d))
return False
return True
-def test_build_tools_exists(c):
- if not test_sdk_exists(c):
- return False
- build_tools = os.path.join(c['sdk_path'], 'build-tools')
- versioned_build_tools = os.path.join(build_tools, c['build_tools'])
+def ensure_build_tools_exists(thisconfig):
+ if not test_sdk_exists(thisconfig):
+ sys.exit(3)
+ build_tools = os.path.join(thisconfig['sdk_path'], 'build-tools')
+ versioned_build_tools = os.path.join(build_tools, thisconfig['build_tools'])
if not os.path.isdir(versioned_build_tools):
logging.critical('Android Build Tools path "'
+ versioned_build_tools + '" does not exist!')
- return False
- return True
+ sys.exit(3)
def write_password_file(pwtype, password=None):
if not vercodes:
return allapps
- apps = [app for app in allapps if app['id'] in vercodes]
+ apps = {}
+ for appid, app in allapps.iteritems():
+ if appid in vercodes:
+ apps[appid] = app
if len(apps) != len(vercodes):
- allids = [app["id"] for app in allapps]
for p in vercodes:
- if p not in allids:
+ if p not in allapps:
logging.critical("No such package: %s" % p)
raise FDroidException("Found invalid app ids in arguments")
if not apps:
raise FDroidException("No packages specified")
error = False
- for app in apps:
- vc = vercodes[app['id']]
+ for appid, app in apps.iteritems():
+ vc = vercodes[appid]
if not vc:
continue
app['builds'] = [b for b in app['builds'] if b['vercode'] in vc]
- if len(app['builds']) != len(vercodes[app['id']]):
+ if len(app['builds']) != len(vercodes[appid]):
error = True
allvcs = [b['vercode'] for b in app['builds']]
- for v in vercodes[app['id']]:
+ for v in vercodes[appid]:
if v not in allvcs:
- logging.critical("No such vercode %s for app %s" % (v, app['id']))
+ logging.critical("No such vercode %s for app %s" % (v, appid))
if error:
raise FDroidException("Found invalid vercodes for some apps")
def getvcs(vcstype, remote, local):
if vcstype == 'git':
return vcs_git(remote, local)
- if vcstype == 'svn':
- return vcs_svn(remote, local)
if vcstype == 'git-svn':
return vcs_gitsvn(remote, local)
if vcstype == 'hg':
if vcstype == 'bzr':
return vcs_bzr(remote, local)
if vcstype == 'srclib':
- if local != 'build/srclib/' + remote:
+ if local != os.path.join('build', 'srclib', remote):
raise VCSException("Error: srclib paths are hard-coded!")
- return getsrclib(remote, 'build/srclib', raw=True)
+ return getsrclib(remote, os.path.join('build', 'srclib'), raw=True)
+ if vcstype == 'svn':
+ raise VCSException("Deprecated vcs type 'svn' - please use 'git-svn' instead")
raise VCSException("Invalid vcs type " + vcstype)
class vcs:
+
def __init__(self, remote, local):
# svn, git-svn and bzr may require auth
self.username = None
- if self.repotype() in ('svn', 'git-svn', 'bzr'):
+ if self.repotype() in ('git-svn', 'bzr'):
if '@' in remote:
self.username, remote = remote.split('@')
if ':' not in self.username:
writeback = False
else:
deleterepo = True
- logging.info(
- "Repository details for %s changed - deleting" % (
- self.local))
+ logging.info("Repository details for %s changed - deleting" % (
+ self.local))
else:
deleterepo = True
logging.info("Repository details for %s missing - deleting" % (
# fdroidserver) and then we'll proceed to destroy it! This is called as
# a safety check.
def checkrepo(self):
- p = SilentPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local)
+ p = FDroidPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local, output=False)
result = p.output.rstrip()
if not result.endswith(self.local):
raise VCSException('Repository mismatch')
else:
self.checkrepo()
# Discard any working tree changes
- p = SilentPopen(['git', 'reset', '--hard'], cwd=self.local)
+ p = FDroidPopen(['git', 'reset', '--hard'], cwd=self.local, output=False)
if p.returncode != 0:
raise VCSException("Git reset failed", p.output)
# Remove untracked files now, in case they're tracked in the target
# revision (it happens!)
- p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
+ p = FDroidPopen(['git', 'clean', '-dffx'], cwd=self.local, output=False)
if p.returncode != 0:
raise VCSException("Git clean failed", p.output)
if not self.refreshed:
p = FDroidPopen(['git', 'fetch', 'origin'], cwd=self.local)
if p.returncode != 0:
raise VCSException("Git fetch failed", p.output)
- p = SilentPopen(['git', 'fetch', '--prune', '--tags', 'origin'], cwd=self.local)
+ p = FDroidPopen(['git', 'fetch', '--prune', '--tags', 'origin'], cwd=self.local, output=False)
if p.returncode != 0:
raise VCSException("Git fetch failed", p.output)
# Recreate origin/HEAD as git clone would do it, in case it disappeared
- p = SilentPopen(['git', 'remote', 'set-head', 'origin', '--auto'], cwd=self.local)
+ p = FDroidPopen(['git', 'remote', 'set-head', 'origin', '--auto'], cwd=self.local, output=False)
if p.returncode != 0:
lines = p.output.splitlines()
if 'Multiple remote HEAD branches' not in lines[0]:
raise VCSException("Git remote set-head failed", p.output)
branch = lines[1].split(' ')[-1]
- p2 = SilentPopen(['git', 'remote', 'set-head', 'origin', branch], cwd=self.local)
+ p2 = FDroidPopen(['git', 'remote', 'set-head', 'origin', branch], cwd=self.local, output=False)
if p2.returncode != 0:
raise VCSException("Git remote set-head failed", p.output + '\n' + p2.output)
self.refreshed = True
# origin/HEAD is the HEAD of the remote, e.g. the "default branch" on
# a github repo. Most of the time this is the same as origin/master.
rev = rev or 'origin/HEAD'
- p = SilentPopen(['git', 'checkout', '-f', rev], cwd=self.local)
+ p = FDroidPopen(['git', 'checkout', '-f', rev], cwd=self.local, output=False)
if p.returncode != 0:
raise VCSException("Git checkout of '%s' failed" % rev, p.output)
# Get rid of any uncontrolled files left behind
- p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
+ p = FDroidPopen(['git', 'clean', '-dffx'], cwd=self.local, output=False)
if p.returncode != 0:
raise VCSException("Git clean failed", p.output)
['git', 'reset', '--hard'],
['git', 'clean', '-dffx'],
]:
- p = SilentPopen(['git', 'submodule', 'foreach', '--recursive'] + cmd, cwd=self.local)
+ p = FDroidPopen(['git', 'submodule', 'foreach', '--recursive'] + cmd, cwd=self.local, output=False)
if p.returncode != 0:
raise VCSException("Git submodule reset failed", p.output)
- p = SilentPopen(['git', 'submodule', 'sync'], cwd=self.local)
+ p = FDroidPopen(['git', 'submodule', 'sync'], cwd=self.local, output=False)
if p.returncode != 0:
raise VCSException("Git submodule sync failed", p.output)
p = FDroidPopen(['git', 'submodule', 'update', '--init', '--force', '--recursive'], cwd=self.local)
def gettags(self):
self.checkrepo()
- p = SilentPopen(['git', 'tag'], cwd=self.local)
+ p = FDroidPopen(['git', 'tag'], cwd=self.local, output=False)
return p.output.splitlines()
def latesttags(self, alltags, number):
self.checkrepo()
- p = SilentPopen(['echo "' + '\n'.join(alltags) + '" | '
- + 'xargs -I@ git log --format=format:"%at @%n" -1 @ | '
- + 'sort -n | awk \'{print $2}\''],
- cwd=self.local, shell=True)
+ p = FDroidPopen(['echo "' + '\n'.join(alltags) + '" | '
+ +
+ 'xargs -I@ git log --format=format:"%at @%n" -1 @ | '
+ + 'sort -n | awk \'{print $2}\''],
+ cwd=self.local, shell=True, output=False)
return p.output.splitlines()[-number:]
# fdroidserver) and then we'll proceed to destory it! This is called as
# a safety check.
def checkrepo(self):
- p = SilentPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local)
+ p = FDroidPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local, output=False)
result = p.output.rstrip()
if not result.endswith(self.local):
raise VCSException('Repository mismatch')
gitsvn_cmd += ' -t %s' % i[5:]
elif i.startswith('branches='):
gitsvn_cmd += ' -b %s' % i[9:]
- p = SilentPopen([gitsvn_cmd + " %s %s" % (remote_split[0], self.local)], shell=True)
+ p = FDroidPopen([gitsvn_cmd + " %s %s" % (remote_split[0], self.local)], shell=True, output=False)
if p.returncode != 0:
self.clone_failed = True
- raise VCSException("Git clone failed", p.output)
+ raise VCSException("Git svn clone failed", p.output)
else:
- p = SilentPopen([gitsvn_cmd + " %s %s" % (self.remote, self.local)], shell=True)
+ p = FDroidPopen([gitsvn_cmd + " %s %s" % (self.remote, self.local)], shell=True, output=False)
if p.returncode != 0:
self.clone_failed = True
- raise VCSException("Git clone failed", p.output)
+ raise VCSException("Git svn clone failed", p.output)
self.checkrepo()
else:
self.checkrepo()
# Discard any working tree changes
- p = SilentPopen(['git', 'reset', '--hard'], cwd=self.local)
+ p = FDroidPopen(['git', 'reset', '--hard'], cwd=self.local, output=False)
if p.returncode != 0:
raise VCSException("Git reset failed", p.output)
# Remove untracked files now, in case they're tracked in the target
# revision (it happens!)
- p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
+ p = FDroidPopen(['git', 'clean', '-dffx'], cwd=self.local, output=False)
if p.returncode != 0:
raise VCSException("Git clean failed", p.output)
if not self.refreshed:
# Get new commits, branches and tags from repo
- p = SilentPopen(['%sgit svn fetch %s' % self.userargs()], cwd=self.local, shell=True)
+ p = FDroidPopen(['%sgit svn fetch %s' % self.userargs()], cwd=self.local, shell=True, output=False)
if p.returncode != 0:
raise VCSException("Git svn fetch failed")
- p = SilentPopen(['%sgit svn rebase %s' % self.userargs()], cwd=self.local, shell=True)
+ p = FDroidPopen(['%sgit svn rebase %s' % self.userargs()], cwd=self.local, shell=True, output=False)
if p.returncode != 0:
raise VCSException("Git svn rebase failed", p.output)
self.refreshed = True
nospaces_rev = rev.replace(' ', '%20')
# Try finding a svn tag
for treeish in ['origin/', '']:
- p = SilentPopen(['git', 'checkout', treeish + 'tags/' + nospaces_rev],
- cwd=self.local)
+ p = FDroidPopen(['git', 'checkout', treeish + 'tags/' + nospaces_rev], cwd=self.local, output=False)
if p.returncode == 0:
break
if p.returncode != 0:
svn_rev = svn_rev if svn_rev[0] == 'r' else 'r' + svn_rev
- p = SilentPopen(['git', 'svn', 'find-rev', svn_rev, treeish],
- cwd=self.local)
+ p = FDroidPopen(['git', 'svn', 'find-rev', '--before', svn_rev, treeish], cwd=self.local, output=False)
git_rev = p.output.rstrip()
if p.returncode == 0 and git_rev:
if p.returncode != 0 or not git_rev:
# Try a plain git checkout as a last resort
- p = SilentPopen(['git', 'checkout', rev], cwd=self.local)
+ p = FDroidPopen(['git', 'checkout', rev], cwd=self.local, output=False)
if p.returncode != 0:
raise VCSException("No git treeish found and direct git checkout of '%s' failed" % rev, p.output)
else:
# Check out the git rev equivalent to the svn rev
- p = SilentPopen(['git', 'checkout', git_rev], cwd=self.local)
+ p = FDroidPopen(['git', 'checkout', git_rev], cwd=self.local, output=False)
if p.returncode != 0:
- raise VCSException("Git svn checkout of '%s' failed" % rev, p.output)
+ raise VCSException("Git checkout of '%s' failed" % rev, p.output)
# Get rid of any uncontrolled files left behind
- p = SilentPopen(['git', 'clean', '-dffx'], cwd=self.local)
+ p = FDroidPopen(['git', 'clean', '-dffx'], cwd=self.local, output=False)
if p.returncode != 0:
raise VCSException("Git clean failed", p.output)
def getref(self):
self.checkrepo()
- p = SilentPopen(['git', 'svn', 'find-rev', 'HEAD'], cwd=self.local)
+ p = FDroidPopen(['git', 'svn', 'find-rev', 'HEAD'], cwd=self.local, output=False)
if p.returncode != 0:
return None
return p.output.strip()
-class vcs_svn(vcs):
-
- def repotype(self):
- return 'svn'
-
- def userargs(self):
- if self.username is None:
- return ['--non-interactive']
- return ['--username', self.username,
- '--password', self.password,
- '--non-interactive']
-
- def gotorevisionx(self, rev):
- if not os.path.exists(self.local):
- p = SilentPopen(['svn', 'checkout', self.remote, self.local] + self.userargs())
- if p.returncode != 0:
- self.clone_failed = True
- raise VCSException("Svn checkout of '%s' failed" % rev, p.output)
- else:
- for svncommand in (
- 'svn revert -R .',
- r"svn status | awk '/\?/ {print $2}' | xargs rm -rf"):
- p = SilentPopen([svncommand], cwd=self.local, shell=True)
- if p.returncode != 0:
- raise VCSException("Svn reset ({0}) failed in {1}".format(svncommand, self.local), p.output)
- if not self.refreshed:
- p = SilentPopen(['svn', 'update'] + self.userargs(), cwd=self.local)
- if p.returncode != 0:
- raise VCSException("Svn update failed", p.output)
- self.refreshed = True
-
- revargs = list(['-r', rev] if rev else [])
- p = SilentPopen(['svn', 'update', '--force'] + revargs + self.userargs(), cwd=self.local)
- if p.returncode != 0:
- raise VCSException("Svn update failed", p.output)
-
- def getref(self):
- p = SilentPopen(['svn', 'info'], cwd=self.local)
- for line in p.output.splitlines():
- if line and line.startswith('Last Changed Rev: '):
- return line[18:]
- return None
-
-
class vcs_hg(vcs):
def repotype(self):
def gotorevisionx(self, rev):
if not os.path.exists(self.local):
- p = SilentPopen(['hg', 'clone', self.remote, self.local])
+ p = FDroidPopen(['hg', 'clone', self.remote, self.local], output=False)
if p.returncode != 0:
self.clone_failed = True
raise VCSException("Hg clone failed", p.output)
else:
- p = SilentPopen(['hg status -uS | xargs rm -rf'], cwd=self.local, shell=True)
+ p = FDroidPopen(['hg status -uS | xargs rm -rf'], cwd=self.local, shell=True, output=False)
if p.returncode != 0:
raise VCSException("Hg clean failed", p.output)
if not self.refreshed:
- p = SilentPopen(['hg', 'pull'], cwd=self.local)
+ p = FDroidPopen(['hg', 'pull'], cwd=self.local, output=False)
if p.returncode != 0:
raise VCSException("Hg pull failed", p.output)
self.refreshed = True
rev = rev or 'default'
if not rev:
return
- p = SilentPopen(['hg', 'update', '-C', rev], cwd=self.local)
+ p = FDroidPopen(['hg', 'update', '-C', rev], cwd=self.local, output=False)
if p.returncode != 0:
raise VCSException("Hg checkout of '%s' failed" % rev, p.output)
- p = SilentPopen(['hg', 'purge', '--all'], cwd=self.local)
+ p = FDroidPopen(['hg', 'purge', '--all'], cwd=self.local, output=False)
# Also delete untracked files, we have to enable purge extension for that:
if "'purge' is provided by the following extension" in p.output:
- with open(self.local + "/.hg/hgrc", "a") as myfile:
+ with open(os.path.join(self.local, '.hg', 'hgrc'), "a") as myfile:
myfile.write("\n[extensions]\nhgext.purge=\n")
- p = SilentPopen(['hg', 'purge', '--all'], cwd=self.local)
+ p = FDroidPopen(['hg', 'purge', '--all'], cwd=self.local, output=False)
if p.returncode != 0:
raise VCSException("HG purge failed", p.output)
elif p.returncode != 0:
raise VCSException("HG purge failed", p.output)
def gettags(self):
- p = SilentPopen(['hg', 'tags', '-q'], cwd=self.local)
+ p = FDroidPopen(['hg', 'tags', '-q'], cwd=self.local, output=False)
return p.output.splitlines()[1:]
def gotorevisionx(self, rev):
if not os.path.exists(self.local):
- p = SilentPopen(['bzr', 'branch', self.remote, self.local])
+ p = FDroidPopen(['bzr', 'branch', self.remote, self.local], output=False)
if p.returncode != 0:
self.clone_failed = True
raise VCSException("Bzr branch failed", p.output)
else:
- p = SilentPopen(['bzr', 'clean-tree', '--force', '--unknown', '--ignored'], cwd=self.local)
+ p = FDroidPopen(['bzr', 'clean-tree', '--force', '--unknown', '--ignored'], cwd=self.local, output=False)
if p.returncode != 0:
raise VCSException("Bzr revert failed", p.output)
if not self.refreshed:
- p = SilentPopen(['bzr', 'pull'], cwd=self.local)
+ p = FDroidPopen(['bzr', 'pull'], cwd=self.local, output=False)
if p.returncode != 0:
raise VCSException("Bzr update failed", p.output)
self.refreshed = True
revargs = list(['-r', rev] if rev else [])
- p = SilentPopen(['bzr', 'revert'] + revargs, cwd=self.local)
+ p = FDroidPopen(['bzr', 'revert'] + revargs, cwd=self.local, output=False)
if p.returncode != 0:
raise VCSException("Bzr revert of '%s' failed" % rev, p.output)
def gettags(self):
- p = SilentPopen(['bzr', 'tags'], cwd=self.local)
+ p = FDroidPopen(['bzr', 'tags'], cwd=self.local, output=False)
return [tag.split(' ')[0].strip() for tag in
p.output.splitlines()]
res_dirs = [
os.path.join(app_dir, 'res'),
- os.path.join(app_dir, 'src/main'),
+ os.path.join(app_dir, 'src', 'main'),
]
if xmlfiles is None:
xmlfiles = []
for res_dir in res_dirs:
for r, d, f in os.walk(res_dir):
- if r.endswith('/values'):
+ if os.path.basename(r) == 'values':
xmlfiles += [os.path.join(r, x) for x in f if x.endswith('.xml')]
string_search = None
if string.startswith('@string/'):
- string_search = re.compile(r'.*name="' + string[8:] + '".*?>([^<]+?)<.*').search
+ string_search = re.compile(r'.*name="' + string[8:] + '".*?>"?([^<]+?)"?<.*').search
elif string.startswith('&') and string.endswith(';'):
string_search = re.compile(r'.*<!ENTITY.*' + string[1:-1] + '.*?"([^"]+?)".*>').search
# Return list of existing files that will be used to find the highest vercode
-def manifest_paths(app_dir, flavour):
+def manifest_paths(app_dir, flavours):
possible_manifests = \
[os.path.join(app_dir, 'AndroidManifest.xml'),
os.path.join(app_dir, 'src', 'AndroidManifest.xml'),
os.path.join(app_dir, 'build.gradle')]
- if flavour:
+ for flavour in flavours:
+ if flavour == 'yes':
+ continue
possible_manifests.append(
os.path.join(app_dir, 'src', flavour, 'AndroidManifest.xml'))
# Retrieve the package name. Returns the name, or None if not found.
-def fetch_real_name(app_dir, flavour):
+def fetch_real_name(app_dir, flavours):
app_search = re.compile(r'.*<application.*').search
name_search = re.compile(r'.*android:label="([^"]+)".*').search
app_found = False
- for f in manifest_paths(app_dir, flavour):
+ for f in manifest_paths(app_dir, flavours):
if not has_extension(f, 'xml'):
continue
logging.debug("fetch_real_name: Checking manifest at " + f)
# Retrieve the version name
-def version_name(original, app_dir, flavour):
- for f in manifest_paths(app_dir, flavour):
+def version_name(original, app_dir, flavours):
+ for f in manifest_paths(app_dir, flavours):
if not has_extension(f, 'xml'):
continue
string = retrieve_string(app_dir, original)
for root, dirs, files in os.walk(root_dir):
if 'AndroidManifest.xml' in files:
path = os.path.join(root, 'AndroidManifest.xml')
- p = SilentPopen(['sed', '-i', 's/android:debuggable="[^"]*"//g', path])
+ p = FDroidPopen(['sed', '-i', 's/android:debuggable="[^"]*"//g', path], output=False)
if p.returncode != 0:
raise BuildException("Failed to remove debuggable flags of %s" % path)
class FDroidException(Exception):
+
def __init__(self, value, detail=None):
self.value = value
self.detail = detail
if build['subdir']:
localprops += [os.path.join(root_dir, 'local.properties')]
for path in localprops:
- if not os.path.isfile(path):
- continue
- logging.info("Updating properties file at %s" % path)
- f = open(path, 'r')
- props = f.read()
- f.close()
- props += '\n'
+ props = ""
+ if os.path.isfile(path):
+ logging.info("Updating local.properties file at %s" % path)
+ f = open(path, 'r')
+ props += f.read()
+ f.close()
+ props += '\n'
+ else:
+ logging.info("Creating local.properties file at %s" % path)
# Fix old-fashioned 'sdk-location' by copying
# from sdk.dir, if necessary
if build['oldsdkloc']:
else:
props += "sdk.dir=%s\n" % config['sdk_path']
props += "sdk-location=%s\n" % config['sdk_path']
- if 'ndk_path' in config:
+ if config['ndk_path']:
# Add ndk location
props += "ndk.dir=%s\n" % config['ndk_path']
props += "ndk-location=%s\n" % config['ndk_path']
f.write(props)
f.close()
- flavour = None
+ flavours = []
if build['type'] == 'gradle':
- flavour = build['gradle']
- if flavour in ['main', 'yes', '']:
- flavour = None
+ flavours = build['gradle']
version_regex = re.compile(r".*'com\.android\.tools\.build:gradle:([^\.]+\.[^\.]+).*'.*")
gradlepluginver = None
if build['target']:
n = build["target"].split('-')[1]
- SilentPopen(['sed', '-i',
+ FDroidPopen(['sed', '-i',
's@compileSdkVersion *[0-9]*@compileSdkVersion ' + n + '@g',
- 'build.gradle'],
- cwd=root_dir)
+ 'build.gradle'], cwd=root_dir, output=False)
# Remove forced debuggable flags
remove_debuggable_flags(root_dir)
# Insert version code and number into the manifest if necessary
if build['forceversion']:
logging.info("Changing the version name")
- for path in manifest_paths(root_dir, flavour):
+ for path in manifest_paths(root_dir, flavours):
if not os.path.isfile(path):
continue
if has_extension(path, 'xml'):
- p = SilentPopen(['sed', '-i',
- 's/android:versionName="[^"]*"/android:versionName="'
- + build['version'] + '"/g',
- path])
+ p = FDroidPopen(['sed', '-i',
+ 's/android:versionName="[^"]*"/android:versionName="' + build['version'] + '"/g',
+ path], output=False)
if p.returncode != 0:
raise BuildException("Failed to amend manifest")
elif has_extension(path, 'gradle'):
- p = SilentPopen(['sed', '-i',
- 's/versionName *=* *"[^"]*"/versionName = "'
- + build['version'] + '"/g',
- path])
+ p = FDroidPopen(['sed', '-i',
+ 's/versionName *=* *"[^"]*"/versionName = "' + build['version'] + '"/g',
+ path], output=False)
if p.returncode != 0:
raise BuildException("Failed to amend build.gradle")
if build['forcevercode']:
logging.info("Changing the version code")
- for path in manifest_paths(root_dir, flavour):
+ for path in manifest_paths(root_dir, flavours):
if not os.path.isfile(path):
continue
if has_extension(path, 'xml'):
- p = SilentPopen(['sed', '-i',
- 's/android:versionCode="[^"]*"/android:versionCode="'
- + build['vercode'] + '"/g',
- path])
+ p = FDroidPopen(['sed', '-i',
+ 's/android:versionCode="[^"]*"/android:versionCode="' + build['vercode'] + '"/g',
+ path], output=False)
if p.returncode != 0:
raise BuildException("Failed to amend manifest")
elif has_extension(path, 'gradle'):
- p = SilentPopen(['sed', '-i',
- 's/versionCode *=* *[0-9]*/versionCode = '
- + build['vercode'] + '/g',
- path])
+ p = FDroidPopen(['sed', '-i',
+ 's/versionCode *=* *[0-9]*/versionCode = ' + build['vercode'] + '/g',
+ path], output=False)
if p.returncode != 0:
raise BuildException("Failed to amend build.gradle")
logging.info("Removing {0}".format(part))
if os.path.lexists(dest):
if os.path.islink(dest):
- SilentPopen(['unlink ' + dest], shell=True)
+ FDroidPopen(['unlink ' + dest], shell=True, output=False)
else:
- SilentPopen(['rm -rf ' + dest], shell=True)
+ FDroidPopen(['rm -rf ' + dest], shell=True, output=False)
else:
logging.info("...but it didn't exist")
# Generate (or update) the ant build file, build.xml...
if build['update'] and build['update'] != ['no'] and build['type'] == 'ant':
- parms = [config['android'], 'update', 'lib-project']
- lparms = [config['android'], 'update', 'project']
+ parms = ['android', 'update', 'lib-project']
+ lparms = ['android', 'update', 'project']
if build['target']:
parms += ['-t', build['target']]
else:
logging.debug("Updating subproject %s" % d)
cmd = lparms + ['-p', d]
- p = FDroidPopen(cmd, cwd=root_dir)
+ p = SdkToolsPopen(cmd, 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)
re.compile(r'crashlytics', re.IGNORECASE),
re.compile(r'ouya.*sdk', re.IGNORECASE),
re.compile(r'libspen23', re.IGNORECASE),
- ]
+ ]
scanignore = getpaths(build_dir, thisbuild, 'scanignore')
scandelete = getpaths(build_dir, thisbuild, 'scandelete')
logging.warn('Found %s at %s' % (what, fd))
def handleproblem(what, fd, fp):
- if todelete(fd):
+ if toignore(fd):
+ logging.info('Ignoring %s at %s' % (what, fd))
+ elif todelete(fd):
removeproblem(what, fd, fp)
else:
logging.error('Found %s at %s' % (what, fd))
fp = os.path.join(r, curfile)
fd = fp[len(build_dir) + 1:]
- # Check if this file has been explicitly excluded from scanning
- if toignore(fd):
- continue
-
- mime = magic.from_file(fp, mime=True) if ms is None else ms.file(fp)
+ try:
+ mime = magic.from_file(fp, mime=True) if ms is None else ms.file(fp)
+ except UnicodeError:
+ warnproblem('malformed magic number', fd)
if mime == 'application/x-sharedlib':
count += handleproblem('shared library', fd, fp)
'application/java-archive',
'application/octet-stream',
'binary',
- ):
+ ):
if has_extension(fp, 'apk'):
removeproblem('APK file', fd, fp)
:param apkfile: full path to the apk to check"""
- p = SilentPopen([os.path.join(config['sdk_path'], 'build-tools',
- config['build_tools'], 'aapt'),
- 'dump', 'xmltree', apkfile, 'AndroidManifest.xml'])
+ p = SdkToolsPopen(['aapt', 'dump', 'xmltree', apkfile, 'AndroidManifest.xml'],
+ output=False)
if p.returncode != 0:
logging.critical("Failed to get apk manifest information")
sys.exit(1)
class AsynchronousFileReader(threading.Thread):
+
'''
Helper class to implement asynchronous reading of a file
in a separate thread. Pushes read lines on a queue to
output = ''
-def SilentPopen(commands, cwd=None, shell=False):
- return FDroidPopen(commands, cwd=cwd, shell=shell, output=False)
+def SdkToolsPopen(commands, cwd=None, shell=False, output=True):
+ cmd = commands[0]
+ if cmd not in config:
+ config[cmd] = find_sdk_tools_cmd(commands[0])
+ return FDroidPopen([config[cmd]] + commands[1:],
+ cwd=cwd, shell=shell, output=output)
def FDroidPopen(commands, cwd=None, shell=False, output=True):
logging.debug("> %s" % ' '.join(commands))
result = PopenResult()
- p = subprocess.Popen(commands, cwd=cwd, shell=shell, env=env,
- stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+ p = None
+ try:
+ p = subprocess.Popen(commands, cwd=cwd, shell=shell, env=env,
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+ except OSError, e:
+ raise BuildException("OSError while trying to execute " + ' '.join(commands) + ': ' + str(e))
stdout_queue = Queue.Queue()
stdout_reader = AsynchronousFileReader(p.stdout, stdout_queue)
time.sleep(0.1)
- p.wait()
- result.returncode = p.returncode
+ result.returncode = p.wait()
return result
re.compile(r'^[\t ]*signingConfig [^ ]*$'),
re.compile(r'.*android\.signingConfigs\.[^{]*$'),
re.compile(r'.*variant\.outputFile = .*'),
+ re.compile(r'.*output\.outputFile = .*'),
re.compile(r'.*\.readLine\(.*'),
- ]
+ ]
for root, dirs, files in os.walk(build_dir):
if 'build.gradle' in files:
path = os.path.join(root, 'build.gradle')
'build.properties',
'default.properties',
'ant.properties',
- ]:
+ ]:
if propfile in files:
path = os.path.join(root, propfile)
o.write(line)
if not placed:
o.write('android.library.reference.%d=%s\n' % (number, relpath))
+
+
+def compare_apks(apk1, apk2, tmp_dir):
+ """Compare two apks
+
+ Returns None if the apk content is the same (apart from the signing key),
+ otherwise a string describing what's different, or what went wrong when
+ trying to do the comparison.
+ """
+
+ thisdir = os.path.join(tmp_dir, 'this_apk')
+ thatdir = os.path.join(tmp_dir, 'that_apk')
+ for d in [thisdir, thatdir]:
+ if os.path.exists(d):
+ shutil.rmtree(d)
+ os.mkdir(d)
+
+ if subprocess.call(['jar', 'xf',
+ os.path.abspath(apk1)],
+ cwd=thisdir) != 0:
+ return("Failed to unpack " + apk1)
+ if subprocess.call(['jar', 'xf',
+ os.path.abspath(apk2)],
+ cwd=thatdir) != 0:
+ return("Failed to unpack " + apk2)
+
+ p = FDroidPopen(['diff', '-r', 'this_apk', 'that_apk'], cwd=tmp_dir,
+ output=False)
+ lines = p.output.splitlines()
+ if len(lines) != 1 or 'META-INF' not in lines[0]:
+ return("Unexpected diff output - " + p.output)
+
+ # If we get here, it seems like they're the same!
+ return None