rewrite docstrings to match numpy style guide

This commit is contained in:
Benedikt Brückmann 2021-06-07 12:26:57 +02:00
parent d168b9c05b
commit 1e943a22df
22 changed files with 559 additions and 396 deletions

View file

@ -425,6 +425,7 @@ Build documentation:
image: python:3.9-buster image: python:3.9-buster
script: script:
- pip install -e .[docs] - pip install -e .[docs]
- pydocstyle fdroidserver
- cd docs - cd docs
- sphinx-apidoc -o ./source ../fdroidserver -M -e - sphinx-apidoc -o ./source ../fdroidserver -M -e
- sphinx-autogen -o generated source/*.rst - sphinx-autogen -o generated source/*.rst

View file

@ -69,9 +69,13 @@ def print_help(available_plugins=None):
def preparse_plugin(module_name, module_dir): def preparse_plugin(module_name, module_dir):
"""simple regex based parsing for plugin scripts, """No summary.
so we don't have to import them when we just need the summary,
but not plan on executing this particular plugin.""" Simple regex based parsing for plugin scripts.
So we don't have to import them when we just need the summary,
but not plan on executing this particular plugin.
"""
if '.' in module_name: if '.' in module_name:
raise ValueError("No '.' allowed in fdroid plugin modules: '{}'" raise ValueError("No '.' allowed in fdroid plugin modules: '{}'"
.format(module_name)) .format(module_name))

View file

@ -13,8 +13,7 @@
# #
# -- ; }}}1 # -- ; }}}1
""" """Copy/extract/patch apk signatures.
copy/extract/patch apk signatures
apksigcopier is a tool for copying APK signatures from a signed APK to an apksigcopier is a tool for copying APK signatures from a signed APK to an
unsigned one (in order to verify reproducible builds). unsigned one (in order to verify reproducible builds).
@ -129,8 +128,7 @@ class APKZipInfo(ReproducibleZipInfo):
def noautoyes(value): def noautoyes(value):
""" """Turn False into NO, None into AUTO, and True into YES.
Turns False into NO, None into AUTO, and True into YES.
>>> from apksigcopier import noautoyes, NO, AUTO, YES >>> from apksigcopier import noautoyes, NO, AUTO, YES
>>> noautoyes(False) == NO == noautoyes(NO) >>> noautoyes(False) == NO == noautoyes(NO)
@ -152,7 +150,8 @@ def noautoyes(value):
def is_meta(filename): def is_meta(filename):
""" """No summary.
Returns whether filename is a v1 (JAR) signature file (.SF), signature block Returns whether filename is a v1 (JAR) signature file (.SF), signature block
file (.RSA, .DSA, or .EC), or manifest (MANIFEST.MF). file (.RSA, .DSA, or .EC), or manifest (MANIFEST.MF).
@ -162,7 +161,7 @@ def is_meta(filename):
def exclude_from_copying(filename): def exclude_from_copying(filename):
"""fdroidserver always wants JAR Signature files to be excluded""" """Fdroidserver always wants JAR Signature files to be excluded."""
return is_meta(filename) return is_meta(filename)
@ -198,17 +197,17 @@ def exclude_from_copying(filename):
# FIXME: makes certain assumptions and doesn't handle all valid ZIP files! # FIXME: makes certain assumptions and doesn't handle all valid ZIP files!
def copy_apk(unsigned_apk, output_apk): def copy_apk(unsigned_apk, output_apk):
""" """Copy APK like apksigner would, excluding files matched by exclude_from_copying().
Copy APK like apksigner would, excluding files matched by
exclude_from_copying().
Returns max date_time.
The following global variables (which default to False), can be set to The following global variables (which default to False), can be set to
override the default behaviour: override the default behaviour:
* set exclude_all_meta=True to exclude all metadata files * set exclude_all_meta=True to exclude all metadata files
* set copy_extra_bytes=True to copy extra bytes after data (e.g. a v2 sig) * set copy_extra_bytes=True to copy extra bytes after data (e.g. a v2 sig)
Returns
-------
max date_time.
""" """
with zipfile.ZipFile(unsigned_apk, "r") as zf: with zipfile.ZipFile(unsigned_apk, "r") as zf:
infos = zf.infolist() infos = zf.infolist()
@ -410,9 +409,10 @@ def patch_v2_sig(extracted_v2_sig, output_apk):
def patch_apk(extracted_meta, extracted_v2_sig, unsigned_apk, output_apk): def patch_apk(extracted_meta, extracted_v2_sig, unsigned_apk, output_apk):
""" """Patch extracted_meta + extracted_v2_sig.
Patch extracted_meta + extracted_v2_sig (if not None) onto unsigned_apk and
save as output_apk. Patches extracted_meta + extracted_v2_sig (if not None)
onto unsigned_apk and save as output_apk.
""" """
date_time = copy_apk(unsigned_apk, output_apk) date_time = copy_apk(unsigned_apk, output_apk)
patch_meta(extracted_meta, output_apk, date_time=date_time) patch_meta(extracted_meta, output_apk, date_time=date_time)
@ -421,8 +421,7 @@ def patch_apk(extracted_meta, extracted_v2_sig, unsigned_apk, output_apk):
def do_extract(signed_apk, output_dir, v1_only=NO): def do_extract(signed_apk, output_dir, v1_only=NO):
""" """Extract signatures from signed_apk and save in output_dir.
Extract signatures from signed_apk and save in output_dir.
The v1_only parameter controls whether the absence of a v1 signature is The v1_only parameter controls whether the absence of a v1 signature is
considered an error or not: considered an error or not:
@ -457,8 +456,7 @@ def do_extract(signed_apk, output_dir, v1_only=NO):
def do_patch(metadata_dir, unsigned_apk, output_apk, v1_only=NO): def do_patch(metadata_dir, unsigned_apk, output_apk, v1_only=NO):
""" """Patch signatures from metadata_dir onto unsigned_apk and save as output_apk.
Patch signatures from metadata_dir onto unsigned_apk and save as output_apk.
The v1_only parameter controls whether the absence of a v1 signature is The v1_only parameter controls whether the absence of a v1 signature is
considered an error or not: considered an error or not:
@ -498,8 +496,7 @@ def do_patch(metadata_dir, unsigned_apk, output_apk, v1_only=NO):
def do_copy(signed_apk, unsigned_apk, output_apk, v1_only=NO): def do_copy(signed_apk, unsigned_apk, output_apk, v1_only=NO):
""" """Copy signatures from signed_apk onto unsigned_apk and save as output_apk.
Copy signatures from signed_apk onto unsigned_apk and save as output_apk.
The v1_only parameter controls whether the absence of a v1 signature is The v1_only parameter controls whether the absence of a v1 signature is
considered an error or not: considered an error or not:

View file

@ -1,9 +1,8 @@
""" """Simple thread based asynchronous file reader for Python.
AsynchronousFileReader AsynchronousFileReader
====================== ======================
Simple thread based asynchronous file reader for Python.
see https://github.com/soxofaan/asynchronousfilereader see https://github.com/soxofaan/asynchronousfilereader
MIT License MIT License
@ -22,10 +21,9 @@ except ImportError:
class AsynchronousFileReader(threading.Thread): class AsynchronousFileReader(threading.Thread):
""" """Helper class to implement asynchronous reading of a file in a separate thread.
Helper class to implement asynchronous reading of a file
in a separate thread. Pushes read lines on a queue to Pushes read lines on a queue to be consumed in another thread.
be consumed in another thread.
""" """
def __init__(self, fd, queue=None, autostart=True): def __init__(self, fd, queue=None, autostart=True):
@ -40,9 +38,7 @@ class AsynchronousFileReader(threading.Thread):
self.start() self.start()
def run(self): def run(self):
""" """Read lines and put them on the queue (the body of the tread)."""
The body of the tread: read lines and put them on the queue.
"""
while True: while True:
line = self._fd.readline() line = self._fd.readline()
if not line: if not line:
@ -50,15 +46,11 @@ class AsynchronousFileReader(threading.Thread):
self.queue.put(line) self.queue.put(line)
def eof(self): def eof(self):
""" """Check whether there is no more content to expect."""
Check whether there is no more content to expect.
"""
return not self.is_alive() and self.queue.empty() return not self.is_alive() and self.queue.empty()
def readlines(self): def readlines(self):
""" """Get currently available lines."""
Get currently available lines.
"""
while not self.queue.empty(): while not self.queue.empty():
yield self.queue.get() yield self.queue.get()

View file

@ -67,7 +67,6 @@ def build_server(app, build, vcs, build_dir, output_dir, log_dir, force):
target folder for the build result target folder for the build result
force force
""" """
global buildserverid global buildserverid
try: try:
@ -325,7 +324,7 @@ def force_gradle_build_tools(build_dir, build_tools):
def transform_first_char(string, method): def transform_first_char(string, method):
"""Uses method() on the first character of string.""" """Use method() on the first character of string."""
if len(string) == 0: if len(string) == 0:
return string return string
if len(string) == 1: if len(string) == 1:
@ -338,11 +337,10 @@ def add_failed_builds_entry(failed_builds, appid, build, entry):
def get_metadata_from_apk(app, build, apkfile): def get_metadata_from_apk(app, build, apkfile):
"""get the required metadata from the built APK """Get the required metadata from the built APK.
versionName is allowed to be a blank string, i.e. '' VersionName is allowed to be a blank string, i.e. ''
""" """
appid, versionCode, versionName = common.get_apk_id(apkfile) appid, versionCode, versionName = common.get_apk_id(apkfile)
native_code = common.get_native_code(apkfile) native_code = common.get_native_code(apkfile)
@ -833,8 +831,7 @@ def build_local(app, build, vcs, build_dir, output_dir, log_dir, srclib_dir, ext
def trybuild(app, build, build_dir, output_dir, log_dir, also_check_dir, def trybuild(app, build, build_dir, output_dir, log_dir, also_check_dir,
srclib_dir, extlib_dir, tmp_dir, repo_dir, vcs, test, srclib_dir, extlib_dir, tmp_dir, repo_dir, vcs, test,
server, force, onserver, refresh): server, force, onserver, refresh):
""" """Build a particular version of an application, if it needs building.
Build a particular version of an application, if it needs building.
Parameters Parameters
---------- ----------
@ -857,7 +854,6 @@ def trybuild(app, build, build_dir, output_dir, log_dir, also_check_dir,
Boolean Boolean
True if the build was done, False if it wasn't necessary. True if the build was done, False if it wasn't necessary.
""" """
dest_file = common.get_release_filename(app, build) dest_file = common.get_release_filename(app, build)
dest = os.path.join(output_dir, dest_file) dest = os.path.join(output_dir, dest_file)
@ -890,7 +886,7 @@ def trybuild(app, build, build_dir, output_dir, log_dir, also_check_dir,
def force_halt_build(timeout): def force_halt_build(timeout):
"""Halt the currently running Vagrant VM, to be called from a Timer""" """Halt the currently running Vagrant VM, to be called from a Timer."""
logging.error(_('Force halting build after {0} sec timeout!').format(timeout)) logging.error(_('Force halting build after {0} sec timeout!').format(timeout))
timeout_event.set() timeout_event.set()
vm = vmtools.get_build_vm('builder') vm = vmtools.get_build_vm('builder')
@ -898,8 +894,13 @@ def force_halt_build(timeout):
def parse_commandline(): def parse_commandline():
"""Parse the command line. Returns options, parser.""" """Parse the command line.
Returns
-------
options
parser
"""
parser = argparse.ArgumentParser(usage="%(prog)s [options] [APPID[:VERCODE] [APPID[:VERCODE] ...]]") parser = argparse.ArgumentParser(usage="%(prog)s [options] [APPID[:VERCODE] [APPID[:VERCODE] ...]]")
common.setup_global_opts(parser) common.setup_global_opts(parser)
parser.add_argument("appid", nargs='*', help=_("application ID with optional versionCode in the form APPID[:VERCODE]")) parser.add_argument("appid", nargs='*', help=_("application ID with optional versionCode in the form APPID[:VERCODE]"))

View file

@ -363,6 +363,7 @@ def check_gplay(app):
def try_init_submodules(app, last_build, vcs): def try_init_submodules(app, last_build, vcs):
"""Try to init submodules if the last build entry used them. """Try to init submodules if the last build entry used them.
They might have been removed from the app's repo in the meantime, They might have been removed from the app's repo in the meantime,
so if we can't find any submodules we continue with the updates check. so if we can't find any submodules we continue with the updates check.
If there is any other error in initializing them then we stop the check. If there is any other error in initializing them then we stop the check.
@ -589,8 +590,7 @@ def checkupdates_app(app):
def status_update_json(processed, failed): def status_update_json(processed, failed):
"""Output a JSON file with metadata about this run""" """Output a JSON file with metadata about this run."""
logging.debug(_('Outputting JSON')) logging.debug(_('Outputting JSON'))
output = common.setup_status_output(start_timestamp) output = common.setup_status_output(start_timestamp)
if processed: if processed:

View file

@ -215,7 +215,7 @@ def _add_java_paths_to_config(pathlist, thisconfig):
def fill_config_defaults(thisconfig): def fill_config_defaults(thisconfig):
"""Fill in the global config dict with relevant defaults """Fill in the global config dict with relevant defaults.
For config values that have a path that can be expanded, e.g. an For config values that have a path that can be expanded, e.g. an
env var or a ~/, this will store the original value using "_orig" env var or a ~/, this will store the original value using "_orig"
@ -480,7 +480,6 @@ def parse_human_readable_size(size):
def assert_config_keystore(config): def assert_config_keystore(config):
"""Check weather keystore is configured correctly and raise exception if not.""" """Check weather keystore is configured correctly and raise exception if not."""
nosigningkey = False nosigningkey = False
if 'repo_keyalias' not in config: if 'repo_keyalias' not in config:
nosigningkey = True nosigningkey = True
@ -507,7 +506,7 @@ def assert_config_keystore(config):
def find_apksigner(config): def find_apksigner(config):
"""Searches for the best version apksigner and adds it to the config. """Search for the best version apksigner and adds it to the config.
Returns the best version of apksigner following this algorithm: Returns the best version of apksigner following this algorithm:
@ -643,7 +642,8 @@ def get_local_metadata_files():
def read_pkg_args(appid_versionCode_pairs, allow_vercodes=False): def read_pkg_args(appid_versionCode_pairs, allow_vercodes=False):
""" """No summary.
Parameters Parameters
---------- ----------
appids appids
@ -780,8 +780,7 @@ apk_release_filename_with_sigfp = re.compile(r'(?P<appid>[a-zA-Z0-9_\.]+)_(?P<ve
def apk_parse_release_filename(apkname): def apk_parse_release_filename(apkname):
"""Parses the name of an APK file according the F-Droids APK naming """Parse the name of an APK file according the F-Droids APK naming scheme.
scheme and returns the tokens.
WARNING: Returned values don't necessarily represent the APKs actual WARNING: Returned values don't necessarily represent the APKs actual
properties, the are just paresed from the file name. properties, the are just paresed from the file name.
@ -823,7 +822,6 @@ def getsrcname(app, build):
def get_build_dir(app): def get_build_dir(app):
"""Get the dir that this app will be built in.""" """Get the dir that this app will be built in."""
if app.RepoType == 'srclib': if app.RepoType == 'srclib':
return Path('build/srclib') / app.Repo return Path('build/srclib') / app.Repo
@ -973,7 +971,9 @@ class vcs:
return None return None
def gotorevision(self, rev, refresh=True): def gotorevision(self, rev, refresh=True):
"""Take the local repository to a clean version of the given """Take the local repository to a clean version of the given revision.
Take the local repository to a clean version of the given
revision, which is specificed in the VCS's native revision, which is specificed in the VCS's native
format. Beforehand, the repository can be dirty, or even format. Beforehand, the repository can be dirty, or even
non-existent. If the repository does already exist locally, it non-existent. If the repository does already exist locally, it
@ -1030,7 +1030,9 @@ class vcs:
raise exc raise exc
def gotorevisionx(self, rev): # pylint: disable=unused-argument def gotorevisionx(self, rev): # pylint: disable=unused-argument
"""Derived classes need to implement this. """No summary.
Derived classes need to implement this.
It's called once basic checking has been performed. It's called once basic checking has been performed.
""" """
@ -1059,7 +1061,7 @@ class vcs:
raise VCSException('getref not supported for this vcs type') raise VCSException('getref not supported for this vcs type')
def getsrclib(self): def getsrclib(self):
"""Returns the srclib (name, path) used in setting up the current revision, or None.""" """Return the srclib (name, path) used in setting up the current revision, or None."""
return self.srclib return self.srclib
@ -1106,7 +1108,9 @@ class vcs_git(vcs):
envs=envs, cwd=cwd, output=output) envs=envs, cwd=cwd, output=output)
def checkrepo(self): def checkrepo(self):
"""If the local directory exists, but is somehow not a git repository, """No summary.
If the local directory exists, but is somehow not a git repository,
git will traverse up the directory tree until it finds one git will traverse up the directory tree until it finds one
that is (i.e. fdroidserver) and then we'll proceed to destroy that is (i.e. fdroidserver) and then we'll proceed to destroy
it! This is called as a safety check. it! This is called as a safety check.
@ -1251,7 +1255,9 @@ class vcs_gitsvn(vcs):
return ['git', 'svn', '--version'] return ['git', 'svn', '--version']
def checkrepo(self): def checkrepo(self):
"""If the local directory exists, but is somehow not a git repository, """No summary.
If the local directory exists, but is somehow not a git repository,
git will traverse up the directory tree until it finds one that git will traverse up the directory tree until it finds one that
is (i.e. fdroidserver) and then we'll proceed to destory it! is (i.e. fdroidserver) and then we'll proceed to destory it!
This is called as a safety check. This is called as a safety check.
@ -1470,7 +1476,7 @@ class vcs_bzr(vcs):
return ['bzr', '--version'] return ['bzr', '--version']
def bzr(self, args, envs=dict(), cwd=None, output=True): def bzr(self, args, envs=dict(), cwd=None, output=True):
'''Prevent bzr from ever using SSH to avoid security vulns''' """Prevent bzr from ever using SSH to avoid security vulns."""
envs.update({ envs.update({
'BZR_SSH': 'false', 'BZR_SSH': 'false',
}) })
@ -1555,7 +1561,6 @@ def retrieve_string_singleline(app_dir, string, xmlfiles=None):
def manifest_paths(app_dir, flavours): def manifest_paths(app_dir, flavours):
"""Return list of existing files that will be used to find the highest vercode.""" """Return list of existing files that will be used to find the highest vercode."""
# TODO: Remove this in Python3.6 # TODO: Remove this in Python3.6
app_dir = str(app_dir) app_dir = str(app_dir)
possible_manifests = \ possible_manifests = \
@ -1653,8 +1658,8 @@ def app_matches_packagename(app, package):
def parse_androidmanifests(paths, app): def parse_androidmanifests(paths, app):
""" """Extract some information from the AndroidManifest.xml at the given path.
Extract some information from the AndroidManifest.xml at the given path.
Returns (version, vercode, package), any or all of which might be None. Returns (version, vercode, package), any or all of which might be None.
All values returned are strings. All values returned are strings.
@ -1662,7 +1667,6 @@ def parse_androidmanifests(paths, app):
this code assumes the files use UTF-8. this code assumes the files use UTF-8.
https://sites.google.com/a/android.com/tools/knownissues/encoding https://sites.google.com/a/android.com/tools/knownissues/encoding
""" """
ignoreversions = app.UpdateCheckIgnore ignoreversions = app.UpdateCheckIgnore
ignoresearch = re.compile(ignoreversions).search if ignoreversions else None ignoresearch = re.compile(ignoreversions).search if ignoreversions else None
@ -1886,7 +1890,7 @@ def get_all_gradle_and_manifests(build_dir):
def get_gradle_subdir(build_dir, paths): def get_gradle_subdir(build_dir, paths):
"""get the subdir where the gradle build is based""" """Get the subdir where the gradle build is based."""
first_gradle_dir = None first_gradle_dir = None
for path in paths: for path in paths:
if not first_gradle_dir: if not first_gradle_dir:
@ -2411,7 +2415,7 @@ def natural_key(s):
def check_system_clock(dt_obj, path): def check_system_clock(dt_obj, path):
"""Check if system clock is updated based on provided date """Check if system clock is updated based on provided date.
If an APK has files newer than the system time, suggest updating If an APK has files newer than the system time, suggest updating
the system clock. This is useful for offline systems, used for the system clock. This is useful for offline systems, used for
@ -2478,8 +2482,12 @@ class KnownApks:
def recordapk(self, apkName, app, default_date=None): def recordapk(self, apkName, app, default_date=None):
""" """
Record an APK (if it's new, otherwise does nothing) Record an APK (if it's new, otherwise does nothing).
Returns the date it was added as a datetime instance
Returns
-------
datetime
the date it was added as a datetime instance.
""" """
if apkName not in self.apks: if apkName not in self.apks:
if default_date is None: if default_date is None:
@ -2490,8 +2498,9 @@ class KnownApks:
return added return added
def getapp(self, apkname): def getapp(self, apkname):
"""Look up information - given the 'apkname', returns (app id, date added/None). """Look up information - given the 'apkname'.
Returns (app id, date added/None).
Or returns None for an unknown apk. Or returns None for an unknown apk.
""" """
if apkname in self.apks: if apkname in self.apks:
@ -2499,7 +2508,7 @@ class KnownApks:
return None return None
def getlatest(self, num): def getlatest(self, num):
"""Get the most recent 'num' apps added to the repo, as a list of package ids with the most recent first""" """Get the most recent 'num' apps added to the repo, as a list of package ids with the most recent first."""
apps = {} apps = {}
for apk, app in self.apks.items(): for apk, app in self.apks.items():
appid, added = app appid, added = app
@ -2523,8 +2532,7 @@ def get_file_extension(filename):
def use_androguard(): def use_androguard():
"""Report if androguard is available, and config its debug logging""" """Report if androguard is available, and config its debug logging."""
try: try:
import androguard import androguard
if use_androguard.show_path: if use_androguard.show_path:
@ -2556,7 +2564,6 @@ def ensure_final_value(packageName, arsc, value):
Resource ID instead of the actual value. This checks whether Resource ID instead of the actual value. This checks whether
the value is actually a resId, then performs the Android the value is actually a resId, then performs the Android
Resource lookup as needed. Resource lookup as needed.
""" """
if value: if value:
returnValue = value returnValue = value
@ -2572,7 +2579,7 @@ def ensure_final_value(packageName, arsc, value):
def is_apk_and_debuggable(apkfile): def is_apk_and_debuggable(apkfile):
"""Returns True if the given file is an APK and is debuggable. """Return True if the given file is an APK and is debuggable.
Parse only <application android:debuggable=""> from the APK. Parse only <application android:debuggable=""> from the APK.
@ -2700,8 +2707,10 @@ def get_apk_id_aapt(apkfile):
def get_native_code(apkfile): def get_native_code(apkfile):
"""aapt checks if there are architecture folders under the lib/ folder """Aapt checks if there are architecture folders under the lib/ folder.
so we are simulating the same behaviour"""
We are simulating the same behaviour.
"""
arch_re = re.compile("^lib/(.*)/.*$") arch_re = re.compile("^lib/(.*)/.*$")
archset = set() archset = set()
with ZipFile(apkfile) as apk: with ZipFile(apkfile) as apk:
@ -3110,7 +3119,7 @@ def metadata_get_sigdir(appid, vercode=None):
def metadata_find_developer_signature(appid, vercode=None): def metadata_find_developer_signature(appid, vercode=None):
"""Tries to find the developer signature for given appid. """Try to find the developer signature for given appid.
This picks the first signature file found in metadata an returns its This picks the first signature file found in metadata an returns its
signature. signature.
@ -3148,7 +3157,7 @@ def metadata_find_developer_signature(appid, vercode=None):
def metadata_find_signing_files(appid, vercode): def metadata_find_signing_files(appid, vercode):
"""Gets a list of signed manifests and signatures. """Get a list of signed manifests and signatures.
Parameters Parameters
---------- ----------
@ -3166,7 +3175,6 @@ def metadata_find_signing_files(appid, vercode):
References References
---------- ----------
* https://docs.oracle.com/javase/tutorial/deployment/jar/intro.html * https://docs.oracle.com/javase/tutorial/deployment/jar/intro.html
* https://source.android.com/security/apksigning/v2 * https://source.android.com/security/apksigning/v2
* https://source.android.com/security/apksigning/v3 * https://source.android.com/security/apksigning/v3
@ -3219,6 +3227,7 @@ class ClonedZipInfo(zipfile.ZipInfo):
cloning ZipInfo entries. https://bugs.python.org/issue43547 cloning ZipInfo entries. https://bugs.python.org/issue43547
""" """
def __init__(self, zinfo): def __init__(self, zinfo):
self.original = zinfo self.original = zinfo
for k in self.__slots__: for k in self.__slots__:
@ -3243,7 +3252,7 @@ def apk_has_v1_signatures(apkfile):
def apk_strip_v1_signatures(signed_apk, strip_manifest=False): def apk_strip_v1_signatures(signed_apk, strip_manifest=False):
"""Removes signatures from APK. """Remove signatures from APK.
Parameters Parameters
---------- ----------
@ -3283,7 +3292,7 @@ def _zipalign(unsigned_apk, aligned_apk):
def apk_implant_signatures(apkpath, outpath, manifest): def apk_implant_signatures(apkpath, outpath, manifest):
"""Implants a signature from metadata into an APK. """Implant a signature from metadata into an APK.
Note: this changes there supplied APK in place. So copy it if you Note: this changes there supplied APK in place. So copy it if you
need the original to be preserved. need the original to be preserved.
@ -3297,7 +3306,6 @@ def apk_implant_signatures(apkpath, outpath, manifest):
References References
---------- ----------
* https://docs.oracle.com/javase/tutorial/deployment/jar/intro.html * https://docs.oracle.com/javase/tutorial/deployment/jar/intro.html
* https://source.android.com/security/apksigning/v2 * https://source.android.com/security/apksigning/v2
* https://source.android.com/security/apksigning/v3 * https://source.android.com/security/apksigning/v3
@ -3308,7 +3316,7 @@ def apk_implant_signatures(apkpath, outpath, manifest):
def apk_extract_signatures(apkpath, outdir): def apk_extract_signatures(apkpath, outdir):
"""Extracts a signature files from APK and puts them into target directory. """Extract a signature files from APK and puts them into target directory.
Parameters Parameters
---------- ----------
@ -3319,7 +3327,6 @@ def apk_extract_signatures(apkpath, outdir):
References References
---------- ----------
* https://docs.oracle.com/javase/tutorial/deployment/jar/intro.html * https://docs.oracle.com/javase/tutorial/deployment/jar/intro.html
* https://source.android.com/security/apksigning/v2 * https://source.android.com/security/apksigning/v2
* https://source.android.com/security/apksigning/v3 * https://source.android.com/security/apksigning/v3
@ -3329,9 +3336,9 @@ def apk_extract_signatures(apkpath, outdir):
def get_min_sdk_version(apk): def get_min_sdk_version(apk):
""" """Wrap the androguard function to always return and int.
This wraps the androguard function to always return and int and fall back to 1
if we can't get a valid minsdk version Fall back to 1 if we can't get a valid minsdk version.
Parameters Parameters
---------- ----------
@ -3450,7 +3457,7 @@ def verify_apks(signed_apk, unsigned_apk, tmp_dir, v1_only=None):
def verify_jar_signature(jar): def verify_jar_signature(jar):
"""Verifies the signature of a given JAR file. """Verify the signature of a given JAR file.
jarsigner is very shitty: unsigned JARs pass as "verified"! So jarsigner is very shitty: unsigned JARs pass as "verified"! So
this has to turn on -strict then check for result 4, since this this has to turn on -strict then check for result 4, since this
@ -3627,8 +3634,9 @@ def compare_apks(apk1, apk2, tmp_dir, log_dir=None):
def set_command_in_config(command): def set_command_in_config(command):
"""Try to find specified command in the path, if it hasn't been """Try to find specified command in the path, if it hasn't been manually set in config.yml.
manually set in config.yml. If found, it is added to the config
If found, it is added to the config
dict. The return value says whether the command is available. dict. The return value says whether the command is available.
""" """
@ -3734,15 +3742,14 @@ def genkeystore(localconfig):
def get_cert_fingerprint(pubkey): def get_cert_fingerprint(pubkey):
"""Generate a certificate fingerprint the same way keytool does it (but with slightly different formatting). """Generate a certificate fingerprint the same way keytool does it (but with slightly different formatting)."""
"""
digest = hashlib.sha256(pubkey).digest() digest = hashlib.sha256(pubkey).digest()
ret = [' '.join("%02X" % b for b in bytearray(digest))] ret = [' '.join("%02X" % b for b in bytearray(digest))]
return " ".join(ret) return " ".join(ret)
def get_certificate(signature_block_file): def get_certificate(signature_block_file):
"""Extracts a DER certificate from JAR Signature's "Signature Block File". """Extract a DER certificate from JAR Signature's "Signature Block File".
Parameters Parameters
---------- ----------
@ -4194,7 +4201,7 @@ def run_yamllint(path, indent=0):
def sha256sum(filename): def sha256sum(filename):
'''Calculate the sha256 of the given file''' """Calculate the sha256 of the given file."""
sha = hashlib.sha256() sha = hashlib.sha256()
with open(filename, 'rb') as f: with open(filename, 'rb') as f:
while True: while True:
@ -4206,7 +4213,7 @@ def sha256sum(filename):
def sha256base64(filename): def sha256base64(filename):
'''Calculate the sha256 of the given file as URL-safe base64''' """Calculate the sha256 of the given file as URL-safe base64."""
hasher = hashlib.sha256() hasher = hashlib.sha256()
with open(filename, 'rb') as f: with open(filename, 'rb') as f:
while True: while True:
@ -4218,7 +4225,7 @@ def sha256base64(filename):
def get_ndk_version(ndk_path): def get_ndk_version(ndk_path):
"""Get the version info from the metadata in the NDK package """Get the version info from the metadata in the NDK package.
Since r11, the info is nice and easy to find in Since r11, the info is nice and easy to find in
sources.properties. Before, there was a kludgey format in sources.properties. Before, there was a kludgey format in
@ -4238,7 +4245,7 @@ def get_ndk_version(ndk_path):
def auto_install_ndk(build): def auto_install_ndk(build):
"""auto-install the NDK in the build, this assumes its in a buildserver guest VM """Auto-install the NDK in the build, this assumes its in a buildserver guest VM.
Download, verify, and install the NDK version as specified via the Download, verify, and install the NDK version as specified via the
"ndk:" field in the build entry. As it uncompresses the zipball, "ndk:" field in the build entry. As it uncompresses the zipball,
@ -4276,11 +4283,10 @@ def auto_install_ndk(build):
def _install_ndk(ndk): def _install_ndk(ndk):
"""Install specified NDK if it is not already installed """Install specified NDK if it is not already installed.
Parameters Parameters
---------- ----------
ndk ndk
The NDK version to install, either in "release" form (r21e) or The NDK version to install, either in "release" form (r21e) or
"revision" form (21.4.7075529). "revision" form (21.4.7075529).

View file

@ -47,14 +47,13 @@ REMOTE_HOSTNAME_REGEX = re.compile(r'\W*\w+\W+(\w+).*')
def update_awsbucket(repo_section): def update_awsbucket(repo_section):
''' """Upload the contents of the directory `repo_section` (including subdirectories) to the AWS S3 "bucket".
Upload the contents of the directory `repo_section` (including
subdirectories) to the AWS S3 "bucket". The contents of that subdir of the The contents of that subdir of the
bucket will first be deleted. bucket will first be deleted.
Requires AWS credentials set in config.yml: awsaccesskeyid, awssecretkey Requires AWS credentials set in config.yml: awsaccesskeyid, awssecretkey
''' """
logging.debug('Syncing "' + repo_section + '" to Amazon S3 bucket "' logging.debug('Syncing "' + repo_section + '" to Amazon S3 bucket "'
+ config['awsbucket'] + '"') + config['awsbucket'] + '"')
@ -65,7 +64,7 @@ def update_awsbucket(repo_section):
def update_awsbucket_s3cmd(repo_section): def update_awsbucket_s3cmd(repo_section):
'''upload using the CLI tool s3cmd, which provides rsync-like sync """Upload using the CLI tool s3cmd, which provides rsync-like sync.
The upload is done in multiple passes to reduce the chance of The upload is done in multiple passes to reduce the chance of
interfering with an existing client-server interaction. In the interfering with an existing client-server interaction. In the
@ -74,8 +73,7 @@ def update_awsbucket_s3cmd(repo_section):
the third/last pass, the indexes are uploaded, and any removed the third/last pass, the indexes are uploaded, and any removed
files are deleted from the server. The last pass is the only pass files are deleted from the server. The last pass is the only pass
to use a full MD5 checksum of all files to detect changes. to use a full MD5 checksum of all files to detect changes.
''' """
logging.debug(_('Using s3cmd to sync with: {url}') logging.debug(_('Using s3cmd to sync with: {url}')
.format(url=config['awsbucket'])) .format(url=config['awsbucket']))
@ -142,14 +140,16 @@ def update_awsbucket_s3cmd(repo_section):
def update_awsbucket_libcloud(repo_section): def update_awsbucket_libcloud(repo_section):
''' """No summary.
Upload the contents of the directory `repo_section` (including Upload the contents of the directory `repo_section` (including
subdirectories) to the AWS S3 "bucket". The contents of that subdir of the subdirectories) to the AWS S3 "bucket".
The contents of that subdir of the
bucket will first be deleted. bucket will first be deleted.
Requires AWS credentials set in config.yml: awsaccesskeyid, awssecretkey Requires AWS credentials set in config.yml: awsaccesskeyid, awssecretkey
''' """
logging.debug(_('using Apache libcloud to sync with {url}') logging.debug(_('using Apache libcloud to sync with {url}')
.format(url=config['awsbucket'])) .format(url=config['awsbucket']))
@ -280,14 +280,14 @@ def update_serverwebroot(serverwebroot, repo_section):
def sync_from_localcopy(repo_section, local_copy_dir): def sync_from_localcopy(repo_section, local_copy_dir):
'''Syncs the repo from "local copy dir" filesystem to this box """Sync the repo from "local copy dir" filesystem to this box.
In setups that use offline signing, this is the last step that In setups that use offline signing, this is the last step that
syncs the repo from the "local copy dir" e.g. a thumb drive to the syncs the repo from the "local copy dir" e.g. a thumb drive to the
repo on the local filesystem. That local repo is then used to repo on the local filesystem. That local repo is then used to
push to all the servers that are configured. push to all the servers that are configured.
''' """
logging.info('Syncing from local_copy_dir to this repo.') logging.info('Syncing from local_copy_dir to this repo.')
# trailing slashes have a meaning in rsync which is not needed here, so # trailing slashes have a meaning in rsync which is not needed here, so
# make sure both paths have exactly one trailing slash # make sure both paths have exactly one trailing slash
@ -302,13 +302,13 @@ def sync_from_localcopy(repo_section, local_copy_dir):
def update_localcopy(repo_section, local_copy_dir): def update_localcopy(repo_section, local_copy_dir):
'''copy data from offline to the "local copy dir" filesystem """Copy data from offline to the "local copy dir" filesystem.
This updates the copy of this repo used to shuttle data from an This updates the copy of this repo used to shuttle data from an
offline signing machine to the online machine, e.g. on a thumb offline signing machine to the online machine, e.g. on a thumb
drive. drive.
''' """
# local_copy_dir is guaranteed to have a trailing slash in main() below # local_copy_dir is guaranteed to have a trailing slash in main() below
common.local_rsync(options, repo_section, local_copy_dir) common.local_rsync(options, repo_section, local_copy_dir)
@ -319,7 +319,7 @@ def update_localcopy(repo_section, local_copy_dir):
def _get_size(start_path='.'): def _get_size(start_path='.'):
'''get size of all files in a dir https://stackoverflow.com/a/1392549''' """Get size of all files in a dir https://stackoverflow.com/a/1392549."""
total_size = 0 total_size = 0
for root, dirs, files in os.walk(start_path): for root, dirs, files in os.walk(start_path):
for f in files: for f in files:
@ -329,7 +329,7 @@ def _get_size(start_path='.'):
def update_servergitmirrors(servergitmirrors, repo_section): def update_servergitmirrors(servergitmirrors, repo_section):
'''update repo mirrors stored in git repos """Update repo mirrors stored in git repos.
This is a hack to use public git repos as F-Droid repos. It This is a hack to use public git repos as F-Droid repos. It
recreates the git repo from scratch each time, so that there is no recreates the git repo from scratch each time, so that there is no
@ -339,7 +339,7 @@ def update_servergitmirrors(servergitmirrors, repo_section):
For history, there is the archive section, and there is the binary For history, there is the archive section, and there is the binary
transparency log. transparency log.
''' """
import git import git
from clint.textui import progress from clint.textui import progress
if config.get('local_copy_dir') \ if config.get('local_copy_dir') \
@ -623,7 +623,7 @@ def upload_apk_to_virustotal(virustotal_apikey, packageName, apkName, hash,
def push_binary_transparency(git_repo_path, git_remote): def push_binary_transparency(git_repo_path, git_remote):
'''push the binary transparency git repo to the specifed remote. """Push the binary transparency git repo to the specifed remote.
If the remote is a local directory, make sure it exists, and is a If the remote is a local directory, make sure it exists, and is a
git repo. This is used to move this git repo from an offline git repo. This is used to move this git repo from an offline
@ -636,7 +636,7 @@ def push_binary_transparency(git_repo_path, git_remote):
case, git_remote is a dir on the local file system, e.g. a thumb case, git_remote is a dir on the local file system, e.g. a thumb
drive. drive.
''' """
import git import git
logging.info(_('Pushing binary transparency log to {url}') logging.info(_('Pushing binary transparency log to {url}')

View file

@ -33,8 +33,7 @@ start_timestamp = time.gmtime()
def status_update_json(signed): def status_update_json(signed):
"""Output a JSON file with metadata about this run""" """Output a JSON file with metadata about this run."""
logging.debug(_('Outputting JSON')) logging.debug(_('Outputting JSON'))
output = common.setup_status_output(start_timestamp) output = common.setup_status_output(start_timestamp)
if signed: if signed:

View file

@ -49,11 +49,17 @@ def make(apps, apks, repodir, archive):
This requires properly initialized options and config objects. This requires properly initialized options and config objects.
:param apps: OrderedDict of apps to go into the index, each app should have Parameters
----------
apps
OrderedDict of apps to go into the index, each app should have
at least one associated apk at least one associated apk
:param apks: list of apks to go into the index apks
:param repodir: the repo directory list of apks to go into the index
:param archive: True if this is the archive repo, False if it's the repodir
the repo directory
archive
True if this is the archive repo, False if it's the
main one. main one.
""" """
from fdroidserver.update import METADATA_VERSION from fdroidserver.update import METADATA_VERSION
@ -583,12 +589,16 @@ def _copy_to_local_copy_dir(repodir, f):
def v1_sort_packages(packages, fdroid_signing_key_fingerprints): def v1_sort_packages(packages, fdroid_signing_key_fingerprints):
"""Sorts the supplied list to ensure a deterministic sort order for """Sort the supplied list to ensure a deterministic sort order for package entries in the index file.
package entries in the index file. This sort-order also expresses
This sort-order also expresses
installation preference to the clients. installation preference to the clients.
(First in this list = first to install) (First in this list = first to install)
:param packages: list of packages which need to be sorted before but into index file. Parameters
----------
packages
list of packages which need to be sorted before but into index file.
""" """
GROUP_DEV_SIGNED = 1 GROUP_DEV_SIGNED = 1
GROUP_FDROID_SIGNED = 2 GROUP_FDROID_SIGNED = 2
@ -618,10 +628,7 @@ def v1_sort_packages(packages, fdroid_signing_key_fingerprints):
def make_v0(apps, apks, repodir, repodict, requestsdict, fdroid_signing_key_fingerprints): def make_v0(apps, apks, repodir, repodict, requestsdict, fdroid_signing_key_fingerprints):
""" """Aka index.jar aka index.xml."""
aka index.jar aka index.xml
"""
doc = Document() doc = Document()
def addElement(name, value, doc, parent): def addElement(name, value, doc, parent):
@ -641,7 +648,7 @@ def make_v0(apps, apks, repodir, repodict, requestsdict, fdroid_signing_key_fing
addElement(name, value, doc, parent) addElement(name, value, doc, parent)
def addElementCheckLocalized(name, app, key, doc, parent, default=''): def addElementCheckLocalized(name, app, key, doc, parent, default=''):
"""Fill in field from metadata or localized block """Fill in field from metadata or localized block.
For name/summary/description, they can come only from the app source, For name/summary/description, they can come only from the app source,
or from a dir in fdroiddata. They can be entirely missing from the or from a dir in fdroiddata. They can be entirely missing from the
@ -652,7 +659,6 @@ def make_v0(apps, apks, repodir, repodict, requestsdict, fdroid_signing_key_fing
alpha- sort order. alpha- sort order.
""" """
el = doc.createElement(name) el = doc.createElement(name)
value = app.get(key) value = app.get(key)
lkey = key[:1].lower() + key[1:] lkey = key[:1].lower() + key[1:]
@ -965,9 +971,12 @@ def make_v0(apps, apks, repodir, repodict, requestsdict, fdroid_signing_key_fing
def extract_pubkey(): def extract_pubkey():
""" """Extract and return the repository's public key from the keystore.
Extracts and returns the repository's public key from the keystore.
:return: public key in hex, repository fingerprint Returns
-------
public key in hex
repository fingerprint
""" """
if 'repo_pubkey' in common.config: if 'repo_pubkey' in common.config:
pubkey = unhexlify(common.config['repo_pubkey']) pubkey = unhexlify(common.config['repo_pubkey'])
@ -991,7 +1000,7 @@ def extract_pubkey():
def get_mirror_service_urls(url): def get_mirror_service_urls(url):
'''Get direct URLs from git service for use by fdroidclient """Get direct URLs from git service for use by fdroidclient.
Via 'servergitmirrors', fdroidserver can create and push a mirror Via 'servergitmirrors', fdroidserver can create and push a mirror
to certain well known git services like gitlab or github. This to certain well known git services like gitlab or github. This
@ -999,8 +1008,7 @@ def get_mirror_service_urls(url):
branch in git. The files are then accessible via alternate URLs, branch in git. The files are then accessible via alternate URLs,
where they are served in their raw format via a CDN rather than where they are served in their raw format via a CDN rather than
from git. from git.
''' """
if url.startswith('git@'): if url.startswith('git@'):
url = re.sub(r'^git@([^:]+):(.+)', r'https://\1/\2', url) url = re.sub(r'^git@([^:]+):(.+)', r'https://\1/\2', url)
@ -1038,15 +1046,19 @@ def get_mirror_service_urls(url):
def download_repo_index(url_str, etag=None, verify_fingerprint=True, timeout=600): def download_repo_index(url_str, etag=None, verify_fingerprint=True, timeout=600):
"""Downloads and verifies index file, then returns its data. """Download and verifies index file, then returns its data.
Downloads the repository index from the given :param url_str and Downloads the repository index from the given :param url_str and
verifies the repository's fingerprint if :param verify_fingerprint verifies the repository's fingerprint if :param verify_fingerprint
is not False. is not False.
:raises: VerificationException() if the repository could not be verified Raises
------
VerificationException() if the repository could not be verified
:return: A tuple consisting of: Returns
-------
A tuple consisting of:
- The index in JSON format or None if the index did not change - The index in JSON format or None if the index did not change
- The new eTag as returned by the HTTP request - The new eTag as returned by the HTTP request
@ -1077,15 +1089,18 @@ def download_repo_index(url_str, etag=None, verify_fingerprint=True, timeout=600
def get_index_from_jar(jarfile, fingerprint=None): def get_index_from_jar(jarfile, fingerprint=None):
"""Returns the data, public key, and fingerprint from index-v1.jar """Return the data, public key, and fingerprint from index-v1.jar.
:param fingerprint is the SHA-256 fingerprint of signing key. Only Parameters
----------
fingerprint is the SHA-256 fingerprint of signing key. Only
hex digits count, all other chars will can be discarded. hex digits count, all other chars will can be discarded.
:raises: VerificationException() if the repository could not be verified Raises
------
VerificationException() if the repository could not be verified
""" """
logging.debug(_('Verifying index signature:')) logging.debug(_('Verifying index signature:'))
common.verify_jar_signature(jarfile) common.verify_jar_signature(jarfile)
with zipfile.ZipFile(jarfile) as jar: with zipfile.ZipFile(jarfile) as jar:
@ -1099,13 +1114,20 @@ def get_index_from_jar(jarfile, fingerprint=None):
def get_public_key_from_jar(jar): def get_public_key_from_jar(jar):
""" """Get the public key and its fingerprint from a JAR file.
Get the public key and its fingerprint from a JAR file.
:raises: VerificationException() if the JAR was not signed exactly once Raises
------
VerificationException() if the JAR was not signed exactly once
:param jar: a zipfile.ZipFile object Parameters
:return: the public key from the jar and its fingerprint ----------
jar
a zipfile.ZipFile object
Returns
-------
the public key from the jar and its fingerprint
""" """
# extract certificate from jar # extract certificate from jar
certs = [n for n in jar.namelist() if common.SIGNATURE_BLOCK_FILE_REGEX.match(n)] certs = [n for n in jar.namelist() if common.SIGNATURE_BLOCK_FILE_REGEX.match(n)]

View file

@ -36,7 +36,7 @@ options = None
def disable_in_config(key, value): def disable_in_config(key, value):
'''write a key/value to the local config.yml, then comment it out''' """Write a key/value to the local config.yml, then comment it out."""
import yaml import yaml
with open('config.yml') as f: with open('config.yml') as f:
data = f.read() data = f.read()

View file

@ -249,7 +249,11 @@ def get_lastbuild(builds):
def check_update_check_data_url(app): def check_update_check_data_url(app):
<<<<<<< HEAD
"""UpdateCheckData must have a valid HTTPS URL to protect checkupdates runs""" """UpdateCheckData must have a valid HTTPS URL to protect checkupdates runs"""
=======
"""UpdateCheckData must have a valid HTTPS URL to protect checkupdates runs."""
>>>>>>> c380427b (rewrite docstrings to match numpy style guide)
if app.UpdateCheckData and app.UpdateCheckMode == 'HTTP': if app.UpdateCheckData and app.UpdateCheckMode == 'HTTP':
urlcode, codeex, urlver, verex = app.UpdateCheckData.split('|') urlcode, codeex, urlver, verex = app.UpdateCheckData.split('|')
for url in (urlcode, urlver): for url in (urlcode, urlver):
@ -503,7 +507,7 @@ def check_format(app):
def check_license_tag(app): def check_license_tag(app):
'''Ensure all license tags contain only valid/approved values''' """Ensure all license tags contain only valid/approved values."""
if config['lint_licenses'] is None: if config['lint_licenses'] is None:
return return
if app.License not in config['lint_licenses']: if app.License not in config['lint_licenses']:
@ -555,8 +559,7 @@ def check_extlib_dir(apps):
def check_app_field_types(app): def check_app_field_types(app):
"""Check the fields have valid data types""" """Check the fields have valid data types."""
for field in app.keys(): for field in app.keys():
v = app.get(field) v = app.get(field)
t = metadata.fieldtype(field) t = metadata.fieldtype(field)
@ -599,7 +602,7 @@ def check_app_field_types(app):
def check_for_unsupported_metadata_files(basedir=""): def check_for_unsupported_metadata_files(basedir=""):
"""Checks whether any non-metadata files are in metadata/""" """Check whether any non-metadata files are in metadata/."""
basedir = Path(basedir) basedir = Path(basedir)
global config global config
@ -633,8 +636,7 @@ def check_for_unsupported_metadata_files(basedir=""):
def check_current_version_code(app): def check_current_version_code(app):
"""Check that the CurrentVersionCode is currently available""" """Check that the CurrentVersionCode is currently available."""
archive_policy = app.get('ArchivePolicy') archive_policy = app.get('ArchivePolicy')
if archive_policy and archive_policy.split()[0] == "0": if archive_policy and archive_policy.split()[0] == "0":
return return

View file

@ -44,7 +44,7 @@ VALID_USERNAME_REGEX = re.compile(r'^[a-z\d](?:[a-z\d/._-]){0,38}$', re.IGNORECA
def _warn_or_exception(value, cause=None): def _warn_or_exception(value, cause=None):
'''output warning or Exception depending on -W''' """Output warning or Exception depending on -W."""
if warnings_action == 'ignore': if warnings_action == 'ignore':
pass pass
elif warnings_action == 'error': elif warnings_action == 'error':
@ -326,7 +326,7 @@ class Build(dict):
return 'ant' return 'ant'
def ndk_path(self): def ndk_path(self):
"""Returns the path to the first configured NDK or an empty string""" """Return the path to the first configured NDK or an empty string."""
ndk = self.ndk ndk = self.ndk
if isinstance(ndk, list): if isinstance(ndk, list):
ndk = self.ndk[0] ndk = self.ndk[0]
@ -368,8 +368,7 @@ def flagtype(name):
class FieldValidator(): class FieldValidator():
""" """Designate App metadata field types and checks that it matches.
Designates App metadata field types and checks that it matches
'name' - The long name of the field type 'name' - The long name of the field type
'matching' - List of possible values or regex expression 'matching' - List of possible values or regex expression
@ -545,7 +544,7 @@ def read_srclibs():
def read_metadata(appids={}, sort_by_time=False): def read_metadata(appids={}, sort_by_time=False):
"""Return a list of App instances sorted newest first """Return a list of App instances sorted newest first.
This reads all of the metadata files in a 'data' repository, then This reads all of the metadata files in a 'data' repository, then
builds a list of App instances from those files. The list is builds a list of App instances from those files. The list is
@ -555,7 +554,6 @@ def read_metadata(appids={}, sort_by_time=False):
appids is a dict with appids a keys and versionCodes as values. appids is a dict with appids a keys and versionCodes as values.
""" """
# Always read the srclibs before the apps, since they can use a srlib as # Always read the srclibs before the apps, since they can use a srlib as
# their source repository. # their source repository.
read_srclibs() read_srclibs()
@ -723,7 +721,7 @@ def _decode_bool(s):
def parse_metadata(metadatapath): def parse_metadata(metadatapath):
"""parse metadata file, also checking the source repo for .fdroid.yml """Parse metadata file, also checking the source repo for .fdroid.yml.
If this is a metadata file from fdroiddata, it will first load the If this is a metadata file from fdroiddata, it will first load the
source repo type and URL from fdroiddata, then read .fdroid.yml if source repo type and URL from fdroiddata, then read .fdroid.yml if
@ -777,7 +775,7 @@ def parse_metadata(metadatapath):
def parse_yaml_metadata(mf, app): def parse_yaml_metadata(mf, app):
"""Parse the .yml file and post-process it """Parse the .yml file and post-process it.
Clean metadata .yml files can be used directly, but in order to Clean metadata .yml files can be used directly, but in order to
make a better user experience for people editing .yml files, there make a better user experience for people editing .yml files, there
@ -787,7 +785,6 @@ def parse_yaml_metadata(mf, app):
overall process. overall process.
""" """
try: try:
yamldata = yaml.load(mf, Loader=SafeLoader) yamldata = yaml.load(mf, Loader=SafeLoader)
except yaml.YAMLError as e: except yaml.YAMLError as e:
@ -836,7 +833,7 @@ def parse_yaml_metadata(mf, app):
def post_parse_yaml_metadata(yamldata): def post_parse_yaml_metadata(yamldata):
"""transform yaml metadata to our internal data format""" """Transform yaml metadata to our internal data format."""
for build in yamldata.get('Builds', []): for build in yamldata.get('Builds', []):
for flag in build.keys(): for flag in build.keys():
_flagtype = flagtype(flag) _flagtype = flagtype(flag)
@ -859,10 +856,13 @@ def post_parse_yaml_metadata(yamldata):
def write_yaml(mf, app): def write_yaml(mf, app):
"""Write metadata in yaml format. """Write metadata in yaml format.
:param mf: active file discriptor for writing Parameters
:param app: app metadata to written to the yaml file ----------
mf
active file discriptor for writing
app
app metadata to written to the yaml file
""" """
# import rumael.yaml and check version # import rumael.yaml and check version
try: try:
import ruamel.yaml import ruamel.yaml
@ -992,6 +992,6 @@ def write_metadata(metadatapath, app):
def add_metadata_arguments(parser): def add_metadata_arguments(parser):
'''add common command line flags related to metadata processing''' """Add common command line flags related to metadata processing."""
parser.add_argument("-W", choices=['error', 'warn', 'ignore'], default='error', parser.add_argument("-W", choices=['error', 'warn', 'ignore'], default='error',
help=_("force metadata errors (default) to be warnings, or to be ignored.")) help=_("force metadata errors (default) to be warnings, or to be ignored."))

View file

@ -75,7 +75,7 @@ def main():
fingerprint = urllib.parse.parse_qs(query).get('fingerprint') fingerprint = urllib.parse.parse_qs(query).get('fingerprint')
def _append_to_url_path(*args): def _append_to_url_path(*args):
'''Append the list of path components to URL, keeping the rest the same''' """Append the list of path components to URL, keeping the rest the same."""
newpath = posixpath.join(path, *args) newpath = posixpath.join(path, *args)
return urllib.parse.urlunparse((scheme, hostname, newpath, params, query, fragment)) return urllib.parse.urlunparse((scheme, hostname, newpath, params, query, fragment))

View file

@ -38,14 +38,20 @@ def download_file(url, local_filename=None, dldir='tmp'):
def http_get(url, etag=None, timeout=600): def http_get(url, etag=None, timeout=600):
""" """Download the content from the given URL by making a GET request.
Downloads the content from the given URL by making a GET request.
If an ETag is given, it will do a HEAD request first, to see if the content changed. If an ETag is given, it will do a HEAD request first, to see if the content changed.
:param url: The URL to download from. Parameters
:param etag: The last ETag to be used for the request (optional). ----------
:return: A tuple consisting of: url
The URL to download from.
etag
The last ETag to be used for the request (optional).
Returns
-------
A tuple consisting of:
- The raw content that was downloaded or None if it did not change - The raw content that was downloaded or None if it did not change
- The new eTag as returned by the HTTP request - The new eTag as returned by the HTTP request
""" """

View file

@ -45,7 +45,6 @@ start_timestamp = time.gmtime()
def publish_source_tarball(apkfilename, unsigned_dir, output_dir): def publish_source_tarball(apkfilename, unsigned_dir, output_dir):
"""Move the source tarball into the output directory...""" """Move the source tarball into the output directory..."""
tarfilename = apkfilename[:-4] + '_src.tar.gz' tarfilename = apkfilename[:-4] + '_src.tar.gz'
tarfile = os.path.join(unsigned_dir, tarfilename) tarfile = os.path.join(unsigned_dir, tarfilename)
if os.path.exists(tarfile): if os.path.exists(tarfile):
@ -56,7 +55,9 @@ def publish_source_tarball(apkfilename, unsigned_dir, output_dir):
def key_alias(appid): def key_alias(appid):
"""Get the alias which F-Droid uses to indentify the singing key """No summary.
Get the alias which F-Droid uses to indentify the singing key
for this App in F-Droids keystore. for this App in F-Droids keystore.
""" """
if config and 'keyaliases' in config and appid in config['keyaliases']: if config and 'keyaliases' in config and appid in config['keyaliases']:
@ -74,9 +75,7 @@ def key_alias(appid):
def read_fingerprints_from_keystore(): def read_fingerprints_from_keystore():
"""Obtain a dictionary containing all singning-key fingerprints which """Obtain a dictionary containing all singning-key fingerprints which are managed by F-Droid, grouped by appid."""
are managed by F-Droid, grouped by appid.
"""
env_vars = {'LC_ALL': 'C.UTF-8', env_vars = {'LC_ALL': 'C.UTF-8',
'FDROID_KEY_STORE_PASS': config['keystorepass']} 'FDROID_KEY_STORE_PASS': config['keystorepass']}
cmd = [config['keytool'], '-list', cmd = [config['keytool'], '-list',
@ -101,8 +100,9 @@ def read_fingerprints_from_keystore():
def sign_sig_key_fingerprint_list(jar_file): def sign_sig_key_fingerprint_list(jar_file):
"""sign the list of app-signing key fingerprints which is """Sign the list of app-signing key fingerprints.
used primaryily by fdroid update to determine which APKs
This is used primaryily by fdroid update to determine which APKs
where built and signed by F-Droid and which ones were where built and signed by F-Droid and which ones were
manually added by users. manually added by users.
""" """
@ -125,6 +125,7 @@ def sign_sig_key_fingerprint_list(jar_file):
def store_stats_fdroid_signing_key_fingerprints(appids, indent=None): def store_stats_fdroid_signing_key_fingerprints(appids, indent=None):
"""Store list of all signing-key fingerprints for given appids to HD. """Store list of all signing-key fingerprints for given appids to HD.
This list will later on be needed by fdroid update. This list will later on be needed by fdroid update.
""" """
if not os.path.exists('stats'): if not os.path.exists('stats'):
@ -143,8 +144,7 @@ def store_stats_fdroid_signing_key_fingerprints(appids, indent=None):
def status_update_json(generatedKeys, signedApks): def status_update_json(generatedKeys, signedApks):
"""Output a JSON file with metadata about this run""" """Output a JSON file with metadata about this run."""
logging.debug(_('Outputting JSON')) logging.debug(_('Outputting JSON'))
output = common.setup_status_output(start_timestamp) output = common.setup_status_output(start_timestamp)
output['apksigner'] = shutil.which(config.get('apksigner', '')) output['apksigner'] = shutil.which(config.get('apksigner', ''))
@ -158,8 +158,8 @@ def status_update_json(generatedKeys, signedApks):
def check_for_key_collisions(allapps): def check_for_key_collisions(allapps):
""" """Make sure there's no collision in keyaliases from apps.
Make sure there's no collision in keyaliases from apps.
It was suggested at It was suggested at
https://dev.guardianproject.info/projects/bazaar/wiki/FDroid_Audit https://dev.guardianproject.info/projects/bazaar/wiki/FDroid_Audit
that a package could be crafted, such that it would use the same signing that a package could be crafted, such that it would use the same signing
@ -168,9 +168,16 @@ def check_for_key_collisions(allapps):
the colliding ID would be something that would be a) a valid package ID, the colliding ID would be something that would be a) a valid package ID,
and b) a sane-looking ID that would make its way into the repo. and b) a sane-looking ID that would make its way into the repo.
Nonetheless, to be sure, before publishing we check that there are no Nonetheless, to be sure, before publishing we check that there are no
collisions, and refuse to do any publishing if that's the case... collisions, and refuse to do any publishing if that's the case.
:param allapps a dict of all apps to process
:return: a list of all aliases corresponding to allapps Parameters
----------
allapps
a dict of all apps to process
Returns
-------
a list of all aliases corresponding to allapps
""" """
allaliases = [] allaliases = []
for appid in allapps: for appid in allapps:
@ -185,9 +192,12 @@ def check_for_key_collisions(allapps):
def create_key_if_not_existing(keyalias): def create_key_if_not_existing(keyalias):
""" """Ensure a signing key with the given keyalias exists.
Ensures a signing key with the given keyalias exists
:return: boolean, True if a new key was created, false otherwise Returns
-------
boolean
True if a new key was created, False otherwise
""" """
# See if we already have a key for this application, and # See if we already have a key for this application, and
# if not generate one... # if not generate one...

View file

@ -104,7 +104,7 @@ def get_gradle_compile_commands(build):
def scan_binary(apkfile): def scan_binary(apkfile):
"""Scan output of apkanalyzer for known non-free classes """Scan output of apkanalyzer for known non-free classes.
apkanalyzer produces useful output when it can run, but it does apkanalyzer produces useful output when it can run, but it does
not support all recent JDK versions, and also some DEX versions, not support all recent JDK versions, and also some DEX versions,
@ -112,7 +112,6 @@ def scan_binary(apkfile):
to run without exiting with an error. to run without exiting with an error.
""" """
logging.info(_('Scanning APK with apkanalyzer for known non-free classes.')) logging.info(_('Scanning APK with apkanalyzer for known non-free classes.'))
result = common.SdkToolsPopen(["apkanalyzer", "dex", "packages", "--defined-only", apkfile], output=False) result = common.SdkToolsPopen(["apkanalyzer", "dex", "packages", "--defined-only", apkfile], output=False)
if result.returncode != 0: if result.returncode != 0:
@ -130,10 +129,12 @@ def scan_binary(apkfile):
def scan_source(build_dir, build=metadata.Build()): def scan_source(build_dir, build=metadata.Build()):
"""Scan the source code in the given directory (and all subdirectories) """Scan the source code in the given directory (and all subdirectories).
and return the number of fatal problems encountered
"""
Returns
-------
the number of fatal problems encountered.
"""
count = 0 count = 0
allowlisted = [ allowlisted = [
@ -193,10 +194,18 @@ def scan_source(build_dir, build=metadata.Build()):
return False return False
def ignoreproblem(what, path_in_build_dir): def ignoreproblem(what, path_in_build_dir):
""" """No summary.
:param what: string describing the problem, will be printed in log messages
:param path_in_build_dir: path to the file relative to `build`-dir Parameters
"returns: 0 as we explicitly ignore the file, so don't count an error ----------
what: string
describing the problem, will be printed in log messages
path_in_build_dir
path to the file relative to `build`-dir
Returns
-------
0 as we explicitly ignore the file, so don't count an error
""" """
msg = ('Ignoring %s at %s' % (what, path_in_build_dir)) msg = ('Ignoring %s at %s' % (what, path_in_build_dir))
logging.info(msg) logging.info(msg)
@ -205,11 +214,20 @@ def scan_source(build_dir, build=metadata.Build()):
return 0 return 0
def removeproblem(what, path_in_build_dir, filepath): def removeproblem(what, path_in_build_dir, filepath):
""" """No summary.
:param what: string describing the problem, will be printed in log messages
:param path_in_build_dir: path to the file relative to `build`-dir Parameters
:param filepath: Path (relative to our current path) to the file ----------
"returns: 0 as we deleted the offending file what: string
describing the problem, will be printed in log messages
path_in_build_dir
path to the file relative to `build`-dir
filepath
Path (relative to our current path) to the file
Returns
-------
0 as we deleted the offending file
""" """
msg = ('Removing %s at %s' % (what, path_in_build_dir)) msg = ('Removing %s at %s' % (what, path_in_build_dir))
logging.info(msg) logging.info(msg)
@ -225,10 +243,18 @@ def scan_source(build_dir, build=metadata.Build()):
return 0 return 0
def warnproblem(what, path_in_build_dir): def warnproblem(what, path_in_build_dir):
""" """No summary.
:param what: string describing the problem, will be printed in log messages
:param path_in_build_dir: path to the file relative to `build`-dir Parameters
:returns: 0, as warnings don't count as errors ----------
what: string
describing the problem, will be printed in log messages
path_in_build_dir
path to the file relative to `build`-dir
Returns
-------
0, as warnings don't count as errors
""" """
if toignore(path_in_build_dir): if toignore(path_in_build_dir):
return 0 return 0
@ -238,13 +264,22 @@ def scan_source(build_dir, build=metadata.Build()):
return 0 return 0
def handleproblem(what, path_in_build_dir, filepath): def handleproblem(what, path_in_build_dir, filepath):
"""Dispatches to problem handlers (ignore, delete, warn) or returns 1 """Dispatches to problem handlers (ignore, delete, warn).
for increasing the error count
:param what: string describing the problem, will be printed in log messages Or returns 1 for increasing the error count.
:param path_in_build_dir: path to the file relative to `build`-dir
:param filepath: Path (relative to our current path) to the file Parameters
:returns: 0 if the problem was ignored/deleted/is only a warning, 1 otherwise ----------
what: string
describing the problem, will be printed in log messages
path_in_build_dir
path to the file relative to `build`-dir
filepath
Path (relative to our current path) to the file
Returns
-------
0 if the problem was ignored/deleted/is only a warning, 1 otherwise
""" """
if toignore(path_in_build_dir): if toignore(path_in_build_dir):
return ignoreproblem(what, path_in_build_dir) return ignoreproblem(what, path_in_build_dir)

View file

@ -32,8 +32,7 @@ start_timestamp = time.gmtime()
def sign_jar(jar): def sign_jar(jar):
""" """Sign a JAR file with Java's jarsigner.
Sign a JAR file with Java's jarsigner.
This method requires a properly initialized config object. This method requires a properly initialized config object.
@ -60,8 +59,7 @@ def sign_jar(jar):
def sign_index_v1(repodir, json_name): def sign_index_v1(repodir, json_name):
""" """Sign index-v1.json to make index-v1.jar.
Sign index-v1.json to make index-v1.jar
This is a bit different than index.jar: instead of their being index.xml This is a bit different than index.jar: instead of their being index.xml
and index_unsigned.jar, the presence of index-v1.json means that there is and index_unsigned.jar, the presence of index-v1.json means that there is
@ -78,8 +76,7 @@ def sign_index_v1(repodir, json_name):
def status_update_json(signed): def status_update_json(signed):
"""Output a JSON file with metadata about this run""" """Output a JSON file with metadata about this run."""
logging.debug(_('Outputting JSON')) logging.debug(_('Outputting JSON'))
output = common.setup_status_output(start_timestamp) output = common.setup_status_output(start_timestamp)
if signed: if signed:

View file

@ -1,23 +1,24 @@
#!/usr/bin/env python #!/usr/bin/env python
''' """Python-Tail - Unix tail follow implementation in Python.
Python-Tail - Unix tail follow implementation in Python.
python-tail can be used to monitor changes to a file. python-tail can be used to monitor changes to a file.
Example: Example
import tail -------
>>> import tail
# Create a tail instance >>>
t = tail.Tail('file-to-be-followed') >>> # Create a tail instance
>>> t = tail.Tail('file-to-be-followed')
# Register a callback function to be called when a new line is found in the followed file. >>>
# If no callback function is registerd, new lines would be printed to standard out. >>> # Register a callback function to be called when a new line is found in the followed file.
t.register_callback(callback_function) >>> # If no callback function is registerd, new lines would be printed to standard out.
>>> t.register_callback(callback_function)
# Follow the file with 5 seconds as sleep time between iterations. >>>
# If sleep time is not provided 1 second is used as the default time. >>> # Follow the file with 5 seconds as sleep time between iterations.
t.follow(s=5) ''' >>> # If sleep time is not provided 1 second is used as the default time.
>>> t.follow(s=5)
"""
# Author - Kasun Herath <kasunh01 at gmail.com> # Author - Kasun Herath <kasunh01 at gmail.com>
# Source - https://github.com/kasun/python-tail # Source - https://github.com/kasun/python-tail
@ -32,42 +33,49 @@ import threading
class Tail(object): class Tail(object):
''' Represents a tail command. ''' """Represents a tail command."""
def __init__(self, tailed_file): def __init__(self, tailed_file):
''' Initiate a Tail instance. """Initiate a Tail instance.
Check for file validity, assigns callback function to standard out. Check for file validity, assigns callback function to standard out.
Arguments: Parameters
tailed_file - File to be followed. ''' ----------
tailed_file
File to be followed.
"""
self.check_file_validity(tailed_file) self.check_file_validity(tailed_file)
self.tailed_file = tailed_file self.tailed_file = tailed_file
self.callback = sys.stdout.write self.callback = sys.stdout.write
self.t_stop = threading.Event() self.t_stop = threading.Event()
def start(self, s=1): def start(self, s=1):
'''Start tailing a file in a background thread. """Start tailing a file in a background thread.
Arguments:
s - Number of seconds to wait between each iteration; Defaults to 3.
'''
Parameters
----------
s
Number of seconds to wait between each iteration; Defaults to 3.
"""
t = threading.Thread(target=self.follow, args=(s,)) t = threading.Thread(target=self.follow, args=(s,))
t.start() t.start()
def stop(self): def stop(self):
'''Stop a background tail. """Stop a background tail."""
'''
self.t_stop.set() self.t_stop.set()
def follow(self, s=1): def follow(self, s=1):
''' Do a tail follow. If a callback function is registered it is called with every new line. """Do a tail follow.
If a callback function is registered it is called with every new line.
Else printed to standard out. Else printed to standard out.
Arguments: Parameters
s - Number of seconds to wait between each iteration; Defaults to 1. ''' ----------
s
Number of seconds to wait between each iteration; Defaults to 1.
"""
with open(self.tailed_file) as file_: with open(self.tailed_file) as file_:
# Go to the end of file # Go to the end of file
file_.seek(0, 2) file_.seek(0, 2)
@ -82,11 +90,11 @@ class Tail(object):
time.sleep(s) time.sleep(s)
def register_callback(self, func): def register_callback(self, func):
''' Overrides default callback function to provided function. ''' """Override default callback function to provided function."""
self.callback = func self.callback = func
def check_file_validity(self, file_): def check_file_validity(self, file_):
''' Check whether the a given file exists, readable and is a file ''' """Check whether the a given file exists, readable and is a file."""
if not os.access(file_, os.F_OK): if not os.access(file_, os.F_OK):
raise TailError("File '%s' does not exist" % (file_)) raise TailError("File '%s' does not exist" % (file_))
if not os.access(file_, os.R_OK): if not os.access(file_, os.R_OK):

View file

@ -128,13 +128,16 @@ def disabled_algorithms_allowed():
def status_update_json(apps, apks): def status_update_json(apps, apks):
"""Output a JSON file with metadata about this `fdroid update` run """Output a JSON file with metadata about this `fdroid update` run.
:param apps: fully populated list of all applications Parameters
:param apks: all to be published apks ----------
apps
fully populated list of all applications
apks
all to be published apks
""" """
logging.debug(_('Outputting JSON')) logging.debug(_('Outputting JSON'))
output = common.setup_status_output(start_timestamp) output = common.setup_status_output(start_timestamp)
output['antiFeatures'] = dict() output['antiFeatures'] = dict()
@ -194,10 +197,14 @@ def status_update_json(apps, apks):
def update_wiki(apps, apks): def update_wiki(apps, apks):
"""Update the wiki """Update the wiki.
:param apps: fully populated list of all applications Parameters
:param apks: all apks, except... ----------
apps
fully populated list of all applications
apks
all apks, except...
""" """
logging.info("Updating wiki") logging.info("Updating wiki")
wikicat = 'Apps' wikicat = 'Apps'
@ -422,9 +429,14 @@ def update_wiki(apps, apks):
def delete_disabled_builds(apps, apkcache, repodirs): def delete_disabled_builds(apps, apkcache, repodirs):
"""Delete disabled build outputs. """Delete disabled build outputs.
:param apps: list of all applications, as per metadata.read_metadata Parameters
:param apkcache: current apk cache information ----------
:param repodirs: the repo directories to process apps
list of all applications, as per metadata.read_metadata
apkcache
current apk cache information
repodirs
the repo directories to process
""" """
for appid, app in apps.items(): for appid, app in apps.items():
for build in app.get('Builds', []): for build in app.get('Builds', []):
@ -480,9 +492,12 @@ def resize_icon(iconpath, density):
def resize_all_icons(repodirs): def resize_all_icons(repodirs):
"""Resize all icons that exceed the max size """Resize all icons that exceed the max size.
:param repodirs: the repo directories to process Parameters
----------
repodirs
the repo directories to process
""" """
for repodir in repodirs: for repodir in repodirs:
for density in screen_densities: for density in screen_densities:
@ -504,12 +519,17 @@ def getsig(apkpath):
md5 digest algorithm. This is not the same as the standard X.509 md5 digest algorithm. This is not the same as the standard X.509
certificate fingerprint. certificate fingerprint.
:param apkpath: path to the apk Parameters
:returns: A string containing the md5 of the signature of the apk or None ----------
apkpath
path to the apk
Returns
-------
A string containing the md5 of the signature of the apk or None
if an error occurred. if an error occurred.
""" """
cert_encoded = common.get_first_signer_certificate(apkpath) cert_encoded = common.get_first_signer_certificate(apkpath)
if not cert_encoded: if not cert_encoded:
return None return None
@ -521,7 +541,7 @@ def get_cache_file():
def get_cache(): def get_cache():
"""Get the cached dict of the APK index """Get the cached dict of the APK index.
Gather information about all the apk files in the repo directory, Gather information about all the apk files in the repo directory,
using cached data if possible. Some of the index operations take a using cached data if possible. Some of the index operations take a
@ -533,7 +553,9 @@ def get_cache():
those cases, there is no easy way to know what has changed from those cases, there is no easy way to know what has changed from
the cache, so just rerun the whole thing. the cache, so just rerun the whole thing.
:return: apkcache Returns
-------
apkcache
""" """
apkcachefile = get_cache_file() apkcachefile = get_cache_file()
@ -582,7 +604,7 @@ def write_cache(apkcache):
def get_icon_bytes(apkzip, iconsrc): def get_icon_bytes(apkzip, iconsrc):
'''ZIP has no official encoding, UTF-* and CP437 are defacto''' """ZIP has no official encoding, UTF-* and CP437 are defacto."""
try: try:
return apkzip.read(iconsrc) return apkzip.read(iconsrc)
except KeyError: except KeyError:
@ -590,7 +612,7 @@ def get_icon_bytes(apkzip, iconsrc):
def has_known_vulnerability(filename): def has_known_vulnerability(filename):
"""checks for known vulnerabilities in the APK """Check for known vulnerabilities in the APK.
Checks OpenSSL .so files in the APK to see if they are a known vulnerable Checks OpenSSL .so files in the APK to see if they are a known vulnerable
version. Google also enforces this: version. Google also enforces this:
@ -603,7 +625,6 @@ def has_known_vulnerability(filename):
Janus is similar to Master Key but is perhaps easier to scan for. Janus is similar to Master Key but is perhaps easier to scan for.
https://www.guardsquare.com/en/blog/new-android-vulnerability-allows-attackers-modify-apps-without-affecting-their-signatures https://www.guardsquare.com/en/blog/new-android-vulnerability-allows-attackers-modify-apps-without-affecting-their-signatures
""" """
found_vuln = False found_vuln = False
# statically load this pattern # statically load this pattern
@ -649,8 +670,9 @@ def has_known_vulnerability(filename):
def insert_obbs(repodir, apps, apks): def insert_obbs(repodir, apps, apks):
"""Scans the .obb files in a given repo directory and adds them to the """Scan the .obb files in a given repo directory and adds them to the relevant APK instances.
relevant APK instances. OBB files have versionCodes like APK
OBB files have versionCodes like APK
files, and they are loosely associated. If there is an OBB file files, and they are loosely associated. If there is an OBB file
present, then any APK with the same or higher versionCode will use present, then any APK with the same or higher versionCode will use
that OBB file. There are two OBB types: main and patch, each APK that OBB file. There are two OBB types: main and patch, each APK
@ -658,12 +680,16 @@ def insert_obbs(repodir, apps, apks):
https://developer.android.com/google/play/expansion-files.html https://developer.android.com/google/play/expansion-files.html
:param repodir: repo directory to scan Parameters
:param apps: list of current, valid apps ----------
:param apks: current information on all APKs repodir
repo directory to scan
apps
list of current, valid apps
apks
current information on all APKs
""" """
def obbWarnDelete(f, msg): def obbWarnDelete(f, msg):
logging.warning(msg + ' ' + f) logging.warning(msg + ' ' + f)
if options.delete_unknown: if options.delete_unknown:
@ -715,7 +741,7 @@ def insert_obbs(repodir, apps, apks):
def translate_per_build_anti_features(apps, apks): def translate_per_build_anti_features(apps, apks):
"""Grab the anti-features list from the build metadata """Grab the anti-features list from the build metadata.
For most Anti-Features, they are really most applicable per-APK, For most Anti-Features, they are really most applicable per-APK,
not for an app. An app can fix a vulnerability, add/remove not for an app. An app can fix a vulnerability, add/remove
@ -729,7 +755,6 @@ def translate_per_build_anti_features(apps, apks):
from the build 'antifeatures' field, not directly included. from the build 'antifeatures' field, not directly included.
""" """
antiFeatures = dict() antiFeatures = dict()
for packageName, app in apps.items(): for packageName, app in apps.items():
d = dict() d = dict()
@ -749,7 +774,7 @@ def translate_per_build_anti_features(apps, apks):
def _get_localized_dict(app, locale): def _get_localized_dict(app, locale):
'''get the dict to add localized store metadata to''' """Get the dict to add localized store metadata to."""
if 'localized' not in app: if 'localized' not in app:
app['localized'] = collections.OrderedDict() app['localized'] = collections.OrderedDict()
if locale not in app['localized']: if locale not in app['localized']:
@ -758,7 +783,7 @@ def _get_localized_dict(app, locale):
def _set_localized_text_entry(app, locale, key, f): def _set_localized_text_entry(app, locale, key, f):
"""Read a fastlane/triple-t metadata file and add an entry to the app """Read a fastlane/triple-t metadata file and add an entry to the app.
This reads more than the limit, in case there is leading or This reads more than the limit, in case there is leading or
trailing whitespace to be stripped trailing whitespace to be stripped
@ -779,7 +804,7 @@ def _set_localized_text_entry(app, locale, key, f):
def _set_author_entry(app, key, f): def _set_author_entry(app, key, f):
"""read a fastlane/triple-t author file and add the entry to the app """Read a fastlane/triple-t author file and add the entry to the app.
This reads more than the limit, in case there is leading or This reads more than the limit, in case there is leading or
trailing whitespace to be stripped trailing whitespace to be stripped
@ -796,7 +821,7 @@ def _set_author_entry(app, key, f):
def _strip_and_copy_image(in_file, outpath): def _strip_and_copy_image(in_file, outpath):
"""Remove any metadata from image and copy it to new path """Remove any metadata from image and copy it to new path.
Sadly, image metadata like EXIF can be used to exploit devices. Sadly, image metadata like EXIF can be used to exploit devices.
It is not used at all in the F-Droid ecosystem, so its much safer It is not used at all in the F-Droid ecosystem, so its much safer
@ -861,8 +886,7 @@ def _strip_and_copy_image(in_file, outpath):
def _get_base_hash_extension(f): def _get_base_hash_extension(f):
'''split a graphic/screenshot filename into base, sha256, and extension """Split a graphic/screenshot filename into base, sha256, and extension."""
'''
base, extension = common.get_extension(f) base, extension = common.get_extension(f)
sha256_index = base.find('_') sha256_index = base.find('_')
if sha256_index > 0: if sha256_index > 0:
@ -871,7 +895,7 @@ def _get_base_hash_extension(f):
def sanitize_funding_yml_entry(entry): def sanitize_funding_yml_entry(entry):
"""FUNDING.yml comes from upstream repos, entries must be sanitized""" """FUNDING.yml comes from upstream repos, entries must be sanitized."""
if type(entry) not in (bytes, int, float, list, str): if type(entry) not in (bytes, int, float, list, str):
return return
if isinstance(entry, bytes): if isinstance(entry, bytes):
@ -894,7 +918,7 @@ def sanitize_funding_yml_entry(entry):
def sanitize_funding_yml_name(name): def sanitize_funding_yml_name(name):
"""Sanitize usernames that come from FUNDING.yml""" """Sanitize usernames that come from FUNDING.yml."""
entry = sanitize_funding_yml_entry(name) entry = sanitize_funding_yml_entry(name)
if entry: if entry:
m = metadata.VALID_USERNAME_REGEX.match(entry) m = metadata.VALID_USERNAME_REGEX.match(entry)
@ -904,7 +928,7 @@ def sanitize_funding_yml_name(name):
def insert_funding_yml_donation_links(apps): def insert_funding_yml_donation_links(apps):
"""include donation links from FUNDING.yml in app's source repo """Include donation links from FUNDING.yml in app's source repo.
GitHub made a standard file format for declaring donation GitHub made a standard file format for declaring donation
links. This parses that format from upstream repos to include in links. This parses that format from upstream repos to include in
@ -917,7 +941,6 @@ def insert_funding_yml_donation_links(apps):
https://help.github.com/en/articles/displaying-a-sponsor-button-in-your-repository#about-funding-files https://help.github.com/en/articles/displaying-a-sponsor-button-in-your-repository#about-funding-files
""" """
if not os.path.isdir('build'): if not os.path.isdir('build'):
return # nothing to do return # nothing to do
for packageName, app in apps.items(): for packageName, app in apps.items():
@ -989,7 +1012,7 @@ def insert_funding_yml_donation_links(apps):
def copy_triple_t_store_metadata(apps): def copy_triple_t_store_metadata(apps):
"""Include store metadata from the app's source repo """Include store metadata from the app's source repo.
The Triple-T Gradle Play Publisher is a plugin that has a standard The Triple-T Gradle Play Publisher is a plugin that has a standard
file layout for all of the metadata and graphics that the Google file layout for all of the metadata and graphics that the Google
@ -1007,7 +1030,6 @@ def copy_triple_t_store_metadata(apps):
https://github.com/Triple-T/gradle-play-publisher/blob/2.1.0/README.md#publishing-listings https://github.com/Triple-T/gradle-play-publisher/blob/2.1.0/README.md#publishing-listings
""" """
if not os.path.isdir('build'): if not os.path.isdir('build'):
return # nothing to do return # nothing to do
@ -1112,7 +1134,7 @@ def copy_triple_t_store_metadata(apps):
def insert_localized_app_metadata(apps): def insert_localized_app_metadata(apps):
"""scans standard locations for graphics and localized text """Scan standard locations for graphics and localized text.
Scans for localized description files, changelogs, store graphics, and Scans for localized description files, changelogs, store graphics, and
screenshots and adds them to the app metadata. Each app's source repo root screenshots and adds them to the app metadata. Each app's source repo root
@ -1139,7 +1161,6 @@ def insert_localized_app_metadata(apps):
See also our documentation page: See also our documentation page:
https://f-droid.org/en/docs/All_About_Descriptions_Graphics_and_Screenshots/#in-the-apps-build-metadata-in-an-fdroiddata-collection https://f-droid.org/en/docs/All_About_Descriptions_Graphics_and_Screenshots/#in-the-apps-build-metadata-in-an-fdroiddata-collection
""" """
sourcedirs = glob.glob(os.path.join('build', '[A-Za-z]*', 'src', '[A-Za-z]*', 'fastlane', 'metadata', 'android', '[a-z][a-z]*')) sourcedirs = glob.glob(os.path.join('build', '[A-Za-z]*', 'src', '[A-Za-z]*', 'fastlane', 'metadata', 'android', '[a-z][a-z]*'))
sourcedirs += glob.glob(os.path.join('build', '[A-Za-z]*', 'fastlane', 'metadata', 'android', '[a-z][a-z]*')) sourcedirs += glob.glob(os.path.join('build', '[A-Za-z]*', 'fastlane', 'metadata', 'android', '[a-z][a-z]*'))
sourcedirs += glob.glob(os.path.join('build', '[A-Za-z]*', 'metadata', '[a-z][a-z]*')) sourcedirs += glob.glob(os.path.join('build', '[A-Za-z]*', 'metadata', '[a-z][a-z]*'))
@ -1259,15 +1280,19 @@ def insert_localized_app_metadata(apps):
def scan_repo_files(apkcache, repodir, knownapks, use_date_from_file=False): def scan_repo_files(apkcache, repodir, knownapks, use_date_from_file=False):
"""Scan a repo for all files with an extension except APK/OBB """Scan a repo for all files with an extension except APK/OBB.
:param apkcache: current cached info about all repo files Parameters
:param repodir: repo directory to scan ----------
:param knownapks: list of all known files, as per metadata.read_metadata apkcache
:param use_date_from_file: use date from file (instead of current date) current cached info about all repo files
for newly added files repodir
repo directory to scan
knownapks
list of all known files, as per metadata.read_metadata
use_date_from_file
use date from file (instead of current date) for newly added files
""" """
cachechanged = False cachechanged = False
repo_files = [] repo_files = []
repodir = repodir.encode() repodir = repodir.encode()
@ -1343,14 +1368,22 @@ def scan_repo_files(apkcache, repodir, knownapks, use_date_from_file=False):
def scan_apk(apk_file): def scan_apk(apk_file):
""" """Scan an APK file and returns dictionary with metadata of the APK.
Scans an APK file and returns dictionary with metadata of the APK.
Attention: This does *not* verify that the APK signature is correct. Attention: This does *not* verify that the APK signature is correct.
:param apk_file: The (ideally absolute) path to the APK file Parameters
:raises BuildException ----------
:return A dict containing APK metadata apk_file
The (ideally absolute) path to the APK file
Raises
------
BuildException
Returns
-------
A dict containing APK metadata
""" """
apk = { apk = {
'hash': common.sha256sum(apk_file), 'hash': common.sha256sum(apk_file),
@ -1397,7 +1430,7 @@ def scan_apk(apk_file):
def _get_apk_icons_src(apkfile, icon_name): def _get_apk_icons_src(apkfile, icon_name):
"""Extract the paths to the app icon in all available densities """Extract the paths to the app icon in all available densities.
The folder name is normally generated by the Android Tools, but The folder name is normally generated by the Android Tools, but
there is nothing that prevents people from using whatever DPI there is nothing that prevents people from using whatever DPI
@ -1423,7 +1456,7 @@ def _get_apk_icons_src(apkfile, icon_name):
def _sanitize_sdk_version(value): def _sanitize_sdk_version(value):
"""Sanitize the raw values from androguard to handle bad values """Sanitize the raw values from androguard to handle bad values.
minSdkVersion/targetSdkVersion/maxSdkVersion must be integers, but minSdkVersion/targetSdkVersion/maxSdkVersion must be integers, but
that doesn't stop devs from doing strange things like setting them that doesn't stop devs from doing strange things like setting them
@ -1564,23 +1597,33 @@ def scan_apk_androguard(apk, apkfile):
def process_apk(apkcache, apkfilename, repodir, knownapks, use_date_from_apk=False, def process_apk(apkcache, apkfilename, repodir, knownapks, use_date_from_apk=False,
allow_disabled_algorithms=False, archive_bad_sig=False): allow_disabled_algorithms=False, archive_bad_sig=False):
"""Processes the apk with the given filename in the given repo directory. """Process the apk with the given filename in the given repo directory.
This also extracts the icons. This also extracts the icons.
:param apkcache: current apk cache information Parameters
:param apkfilename: the filename of the apk to scan ----------
:param repodir: repo directory to scan apkcache
:param knownapks: known apks info current apk cache information
:param use_date_from_apk: use date from APK (instead of current date) apkfilename
for newly added APKs the filename of the apk to scan
:param allow_disabled_algorithms: allow APKs with valid signatures that include repodir
repo directory to scan
knownapks
known apks info
use_date_from_apk
use date from APK (instead of current date) for newly added APKs
allow_disabled_algorithms
allow APKs with valid signatures that include
disabled algorithms in the signature (e.g. MD5) disabled algorithms in the signature (e.g. MD5)
:param archive_bad_sig: move APKs with a bad signature to the archive archive_bad_sig
:returns: (skip, apk, cachechanged) where skip is a boolean indicating whether to skip this apk, move APKs with a bad signature to the archive
Returns
-------
(skip, apk, cachechanged) where skip is a boolean indicating whether to skip this apk,
apk is the scanned apk information, and cachechanged is True if the apkcache got changed. apk is the scanned apk information, and cachechanged is True if the apkcache got changed.
""" """
apk = {} apk = {}
apkfile = os.path.join(repodir, apkfilename) apkfile = os.path.join(repodir, apkfilename)
@ -1699,19 +1742,26 @@ def process_apk(apkcache, apkfilename, repodir, knownapks, use_date_from_apk=Fal
def process_apks(apkcache, repodir, knownapks, use_date_from_apk=False): def process_apks(apkcache, repodir, knownapks, use_date_from_apk=False):
"""Processes the apks in the given repo directory. """Process the apks in the given repo directory.
This also extracts the icons. This also extracts the icons.
:param apkcache: current apk cache information Parameters
:param repodir: repo directory to scan ----------
:param knownapks: known apks info apkcache
:param use_date_from_apk: use date from APK (instead of current date) current apk cache information
for newly added APKs repodir
:returns: (apks, cachechanged) where apks is a list of apk information, repo directory to scan
knownapks
b known apks info
use_date_from_apk
use date from APK (instead of current date) for newly added APKs
Returns
-------
(apks, cachechanged) where apks is a list of apk information,
and cachechanged is True if the apkcache got changed. and cachechanged is True if the apkcache got changed.
""" """
cachechanged = False cachechanged = False
for icon_dir in get_all_icon_dirs(repodir): for icon_dir in get_all_icon_dirs(repodir):
@ -1737,19 +1787,28 @@ def process_apks(apkcache, repodir, knownapks, use_date_from_apk=False):
def extract_apk_icons(icon_filename, apk, apkzip, repo_dir): def extract_apk_icons(icon_filename, apk, apkzip, repo_dir):
"""Extracts PNG icons from an APK with the supported pixel densities """Extract PNG icons from an APK with the supported pixel densities.
Extracts icons from the given APK zip in various densities, saves Extracts icons from the given APK zip in various densities, saves
them into given repo directory and stores their names in the APK them into given repo directory and stores their names in the APK
metadata dictionary. If the icon is an XML icon, then this tries metadata dictionary. If the icon is an XML icon, then this tries
to find PNG icon that can replace it. to find PNG icon that can replace it.
:param icon_filename: A string representing the icon's file name Parameters
:param apk: A populated dictionary containing APK metadata. ----------
icon_filename
A string representing the icon's file name
apk
A populated dictionary containing APK metadata.
Needs to have 'icons_src' key Needs to have 'icons_src' key
:param apkzip: An opened zipfile.ZipFile of the APK file apkzip
:param repo_dir: The directory of the APK's repository An opened zipfile.ZipFile of the APK file
:return: A list of icon densities that are missing repo_dir
The directory of the APK's repository
Returns
-------
A list of icon densities that are missing
""" """
res_name_re = re.compile(r'res/(drawable|mipmap)-(x*[hlm]dpi|anydpi).*/(.*)_[0-9]+dp.(png|xml)') res_name_re = re.compile(r'res/(drawable|mipmap)-(x*[hlm]dpi|anydpi).*/(.*)_[0-9]+dp.(png|xml)')
@ -1820,13 +1879,14 @@ def extract_apk_icons(icon_filename, apk, apkzip, repo_dir):
def fill_missing_icon_densities(empty_densities, icon_filename, apk, repo_dir): def fill_missing_icon_densities(empty_densities, icon_filename, apk, repo_dir):
""" """Resize existing PNG icons for densities missing in the APK to ensure all densities are available.
Resize existing PNG icons for densities missing in the APK to ensure all densities are available
:param empty_densities: A list of icon densities that are missing Parameters
:param icon_filename: A string representing the icon's file name ----------
:param apk: A populated dictionary containing APK metadata. Needs to have 'icons' key empty_densities: A list of icon densities that are missing
:param repo_dir: The directory of the APK's repository icon_filename: A string representing the icon's file name
apk: A populated dictionary containing APK metadata. Needs to have 'icons' key
repo_dir: The directory of the APK's repository
""" """
# First try resizing down to not lose quality # First try resizing down to not lose quality
@ -1889,8 +1949,10 @@ def fill_missing_icon_densities(empty_densities, icon_filename, apk, repo_dir):
def apply_info_from_latest_apk(apps, apks): def apply_info_from_latest_apk(apps, apks):
""" """No summary.
Some information from the apks needs to be applied up to the application level. Some information from the apks needs to be applied up to the application level.
When doing this, we use the info from the most recent version's apk. When doing this, we use the info from the most recent version's apk.
We deal with figuring out when the app was added and last updated at the same time. We deal with figuring out when the app was added and last updated at the same time.
""" """
@ -1920,7 +1982,7 @@ def apply_info_from_latest_apk(apps, apks):
def make_categories_txt(repodir, categories): def make_categories_txt(repodir, categories):
'''Write a category list in the repo to allow quick access''' """Write a category list in the repo to allow quick access."""
catdata = '' catdata = ''
for cat in sorted(categories): for cat in sorted(categories):
catdata += cat + '\n' catdata += cat + '\n'
@ -1982,8 +2044,7 @@ def archive_old_apks(apps, apks, archapks, repodir, archivedir, defaultkeepversi
def move_apk_between_sections(from_dir, to_dir, apk): def move_apk_between_sections(from_dir, to_dir, apk):
"""move an APK from repo to archive or vice versa""" """Move an APK from repo to archive or vice versa."""
def _move_file(from_dir, to_dir, filename, ignore_missing): def _move_file(from_dir, to_dir, filename, ignore_missing):
from_path = os.path.join(from_dir, filename) from_path = os.path.join(from_dir, filename)
if ignore_missing and not os.path.exists(from_path): if ignore_missing and not os.path.exists(from_path):
@ -2033,15 +2094,14 @@ def add_apks_to_per_app_repos(repodir, apks):
def create_metadata_from_template(apk): def create_metadata_from_template(apk):
'''create a new metadata file using internal or external template """Create a new metadata file using internal or external template.
Generate warnings for apk's with no metadata (or create skeleton Generate warnings for apk's with no metadata (or create skeleton
metadata files, if requested on the command line). Though the metadata files, if requested on the command line). Though the
template file is YAML, this uses neither pyyaml nor ruamel.yaml template file is YAML, this uses neither pyyaml nor ruamel.yaml
since those impose things on the metadata file made from the since those impose things on the metadata file made from the
template: field sort order, empty field value, formatting, etc. template: field sort order, empty field value, formatting, etc.
''' """
if os.path.exists('template.yml'): if os.path.exists('template.yml'):
with open('template.yml') as f: with open('template.yml') as f:
metatxt = f.read() metatxt = f.read()
@ -2086,7 +2146,8 @@ def create_metadata_from_template(apk):
def read_added_date_from_all_apks(apps, apks): def read_added_date_from_all_apks(apps, apks):
""" """No summary.
Added dates come from the stats/known_apks.txt file but are Added dates come from the stats/known_apks.txt file but are
read when scanning apks and thus need to be applied form apk read when scanning apks and thus need to be applied form apk
level to app level for _all_ apps and not only from non-archived level to app level for _all_ apps and not only from non-archived
@ -2107,7 +2168,7 @@ def read_added_date_from_all_apks(apps, apks):
def insert_missing_app_names_from_apks(apps, apks): def insert_missing_app_names_from_apks(apps, apks):
"""Use app name from APK if it is not set in the metadata """Use app name from APK if it is not set in the metadata.
Name -> localized -> from APK Name -> localized -> from APK
@ -2148,7 +2209,7 @@ def insert_missing_app_names_from_apks(apps, apks):
def get_apps_with_packages(apps, apks): def get_apps_with_packages(apps, apks):
"""Returns a deepcopy of that subset apps that actually has any associated packages. Skips disabled apps.""" """Return a deepcopy of that subset apps that actually has any associated packages. Skips disabled apps."""
appsWithPackages = collections.OrderedDict() appsWithPackages = collections.OrderedDict()
for packageName in apps: for packageName in apps:
app = apps[packageName] app = apps[packageName]
@ -2165,12 +2226,20 @@ def get_apps_with_packages(apps, apks):
def prepare_apps(apps, apks, repodir): def prepare_apps(apps, apks, repodir):
"""Encapsulates all necessary preparation steps before we can build an index out of apps and apks. """Encapsulate all necessary preparation steps before we can build an index out of apps and apks.
:param apps: All apps as read from metadata Parameters
:param apks: list of apks that belong into repo, this gets modified in place ----------
:param repodir: the target repository directory, metadata files will be copied here apps
:return: the relevant subset of apps (as a deepcopy) All apps as read from metadata
apks
list of apks that belong into repo, this gets modified in place
repodir
the target repository directory, metadata files will be copied here
Returns
-------
the relevant subset of apps (as a deepcopy)
""" """
apps_with_packages = get_apps_with_packages(apps, apks) apps_with_packages = get_apps_with_packages(apps, apks)
apply_info_from_latest_apk(apps_with_packages, apks) apply_info_from_latest_apk(apps_with_packages, apks)

View file

@ -69,7 +69,7 @@ class Decoder(json.JSONDecoder):
def _add_diffoscope_info(d): def _add_diffoscope_info(d):
"""Add diffoscope setup metadata to provided dict under 'diffoscope' key """Add diffoscope setup metadata to provided dict under 'diffoscope' key.
The imports are broken out at stages since various versions of The imports are broken out at stages since various versions of
diffoscope support various parts of these. diffoscope support various parts of these.
@ -112,7 +112,7 @@ def _add_diffoscope_info(d):
def write_json_report(url, remote_apk, unsigned_apk, compare_result): def write_json_report(url, remote_apk, unsigned_apk, compare_result):
"""write out the results of the verify run to JSON """Write out the results of the verify run to JSON.
This builds up reports on the repeated runs of `fdroid verify` on This builds up reports on the repeated runs of `fdroid verify` on
a set of apps. It uses the timestamps on the compared files to a set of apps. It uses the timestamps on the compared files to
@ -120,7 +120,6 @@ def write_json_report(url, remote_apk, unsigned_apk, compare_result):
repeatedly. repeatedly.
""" """
jsonfile = unsigned_apk + '.json' jsonfile = unsigned_apk + '.json'
if os.path.exists(jsonfile): if os.path.exists(jsonfile):
with open(jsonfile) as fp: with open(jsonfile) as fp:

View file

@ -79,15 +79,24 @@ def _check_output(cmd, cwd=None):
def get_build_vm(srvdir, provider=None): def get_build_vm(srvdir, provider=None):
"""Factory function for getting FDroidBuildVm instances. """No summary.
Factory function for getting FDroidBuildVm instances.
This function tries to figure out what hypervisor should be used This function tries to figure out what hypervisor should be used
and creates an object for controlling a build VM. and creates an object for controlling a build VM.
:param srvdir: path to a directory which contains a Vagrantfile Parameters
:param provider: optionally this parameter allows specifiying an ----------
srvdir
path to a directory which contains a Vagrantfile
provider
optionally this parameter allows specifiying an
specific vagrant provider. specific vagrant provider.
:returns: FDroidBuildVm instance.
Returns
-------
FDroidBuildVm instance.
""" """
abssrvdir = abspath(srvdir) abssrvdir = abspath(srvdir)
@ -171,9 +180,9 @@ class FDroidBuildVm():
This is intended to be a hypervisor independent, fault tolerant This is intended to be a hypervisor independent, fault tolerant
wrapper around the vagrant functions we use. wrapper around the vagrant functions we use.
""" """
def __init__(self, srvdir): def __init__(self, srvdir):
"""Create new server class. """Create new server class."""
"""
self.srvdir = srvdir self.srvdir = srvdir
self.srvname = basename(srvdir) + '_default' self.srvname = basename(srvdir) + '_default'
self.vgrntfile = os.path.join(srvdir, 'Vagrantfile') self.vgrntfile = os.path.join(srvdir, 'Vagrantfile')
@ -252,7 +261,7 @@ class FDroidBuildVm():
self.vgrnt.package(output=output) self.vgrnt.package(output=output)
def vagrant_uuid_okay(self): def vagrant_uuid_okay(self):
'''Having an uuid means that vagrant up has run successfully.''' """Having an uuid means that vagrant up has run successfully."""
if self.srvuuid is None: if self.srvuuid is None:
return False return False
return True return True
@ -282,9 +291,14 @@ class FDroidBuildVm():
def box_add(self, boxname, boxfile, force=True): def box_add(self, boxname, boxfile, force=True):
"""Add vagrant box to vagrant. """Add vagrant box to vagrant.
:param boxname: name assigned to local deployment of box Parameters
:param boxfile: path to box file ----------
:param force: overwrite existing box image (default: True) boxname
name assigned to local deployment of box
boxfile
path to box file
force
overwrite existing box image (default: True)
""" """
boxfile = abspath(boxfile) boxfile = abspath(boxfile)
if not isfile(boxfile): if not isfile(boxfile):
@ -304,10 +318,11 @@ class FDroidBuildVm():
shutil.rmtree(boxpath) shutil.rmtree(boxpath)
def sshinfo(self): def sshinfo(self):
"""Get ssh connection info for a vagrant VM """Get ssh connection info for a vagrant VM.
:returns: A dictionary containing 'hostname', 'port', 'user' Returns
and 'idfile' -------
A dictionary containing 'hostname', 'port', 'user' and 'idfile'
""" """
import paramiko import paramiko
try: try: