def adapt_gradle(build_dir): filename = 'build.gradle' for root, dirs, files in os.walk(build_dir): for filename in files: if not filename.endswith('.gradle'): continue path = os.path.join(root, filename) if not os.path.isfile(path): continue logging.debug("Adapting %s at %s" % (filename, path)) common.regsub_file( r"""(\s*)buildToolsVersion([\s=]+)['"].*""", r"""\1buildToolsVersion\2'%s'""" % config['build_tools'], path)
def adapt_gradle(build_dir): filename = 'build.gradle' for root, dirs, files in os.walk(build_dir): for filename in files: if not filename.endswith('.gradle'): continue path = os.path.join(root, filename) if not os.path.isfile(path): continue logging.debug("Adapting %s at %s" % (filename, path)) common.regsub_file(r"""(\s*)buildToolsVersion([\s=]+).*""", r"""\1buildToolsVersion\2'%s'""" % config['build_tools'], path)
def build_local(app, thisbuild, vcs, build_dir, output_dir, srclib_dir, extlib_dir, tmp_dir, force, onserver, refresh): """Do a build locally.""" if thisbuild['buildjni'] and thisbuild['buildjni'] != ['no']: if not thisbuild['ndk_path']: logging.critical("Android NDK version '%s' could not be found!" % thisbuild['ndk']) logging.critical("Configured versions:") for k, v in config['ndk_paths'].iteritems(): if k.endswith("_orig"): continue logging.critical(" %s: %s" % (k, v)) sys.exit(3) elif not os.path.isdir(thisbuild['ndk_path']): logging.critical("Android NDK '%s' is not a directory!" % thisbuild['ndk_path']) sys.exit(3) # Set up environment vars that depend on each build for n in ['ANDROID_NDK', 'NDK', 'ANDROID_NDK_HOME']: common.env[n] = thisbuild['ndk_path'] common.reset_env_path() # Set up the current NDK to the PATH common.add_to_env_path(thisbuild['ndk_path']) # Prepare the source code... root_dir, srclibpaths = common.prepare_source(vcs, app, thisbuild, build_dir, srclib_dir, extlib_dir, onserver, refresh) # We need to clean via the build tool in case the binary dirs are # different from the default ones p = None gradletasks = [] if thisbuild['type'] == 'maven': logging.info("Cleaning Maven project...") cmd = [ config['mvn3'], 'clean', '-Dandroid.sdk.path=' + config['sdk_path'] ] if '@' in thisbuild['maven']: maven_dir = os.path.join(root_dir, thisbuild['maven'].split('@', 1)[1]) maven_dir = os.path.normpath(maven_dir) else: maven_dir = root_dir p = FDroidPopen(cmd, cwd=maven_dir) elif thisbuild['type'] == 'gradle': logging.info("Cleaning Gradle project...") if thisbuild['preassemble']: gradletasks += thisbuild['preassemble'] flavours = thisbuild['gradle'] if flavours == ['yes']: flavours = [] flavours_cmd = ''.join([capitalize_intact(f) for f in flavours]) gradletasks += ['assemble' + flavours_cmd + 'Release'] adapt_gradle(build_dir) for name, number, libpath in srclibpaths: adapt_gradle(libpath) cmd = [config['gradle']] if thisbuild['gradleprops']: cmd += ['-P' + kv for kv in thisbuild['gradleprops']] for task in gradletasks: parts = task.split(':') parts[-1] = 'clean' + capitalize_intact(parts[-1]) cmd += [':'.join(parts)] cmd += ['clean'] p = FDroidPopen(cmd, cwd=root_dir) elif thisbuild['type'] == 'kivy': pass elif thisbuild['type'] == 'ant': logging.info("Cleaning Ant project...") p = FDroidPopen(['ant', 'clean'], cwd=root_dir) if p is not None and p.returncode != 0: raise BuildException( "Error cleaning %s:%s" % (app['id'], thisbuild['version']), p.output) for root, dirs, files in os.walk(build_dir): # Don't remove possibly necessary 'gradle' dirs if 'gradlew' is not there if 'gradlew' in files: logging.debug("Getting rid of Gradle wrapper stuff in %s" % root) os.remove(os.path.join(root, 'gradlew')) if 'gradlew.bat' in files: os.remove(os.path.join(root, 'gradlew.bat')) if 'gradle' in dirs: shutil.rmtree(os.path.join(root, 'gradle')) if options.skipscan: if thisbuild['scandelete']: raise BuildException( "Refusing to skip source scan since scandelete is present") else: # Scan before building... logging.info("Scanning source for common problems...") count = common.scan_source(build_dir, root_dir, thisbuild) if count > 0: if force: logging.warn('Scanner found %d problems' % count) else: raise BuildException( "Can't build due to %d errors while scanning" % count) if not options.notarball: # Build the source tarball right before we build the release... logging.info("Creating source tarball...") tarname = common.getsrcname(app, thisbuild) tarball = tarfile.open(os.path.join(tmp_dir, tarname), "w:gz") def tarexc(f): return any(f.endswith(s) for s in ['.svn', '.git', '.hg', '.bzr']) tarball.add(build_dir, tarname, exclude=tarexc) tarball.close() # Run a build command if one is required... if thisbuild['build']: logging.info("Running 'build' commands in %s" % root_dir) cmd = common.replace_config_vars(thisbuild['build'], thisbuild) # Substitute source library paths into commands... for name, number, libpath in srclibpaths: libpath = os.path.relpath(libpath, root_dir) cmd = cmd.replace('$$' + name + '$$', libpath) p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir) if p.returncode != 0: raise BuildException( "Error running build command for %s:%s" % (app['id'], thisbuild['version']), p.output) # Build native stuff if required... if thisbuild['buildjni'] and thisbuild['buildjni'] != ['no']: logging.info("Building the native code") jni_components = thisbuild['buildjni'] if jni_components == ['yes']: jni_components = [''] cmd = [os.path.join(thisbuild['ndk_path'], "ndk-build"), "-j1"] for d in jni_components: if d: logging.info("Building native code in '%s'" % d) else: logging.info("Building native code in the main project") manifest = root_dir + '/' + d + '/AndroidManifest.xml' if os.path.exists(manifest): # Read and write the whole AM.xml to fix newlines and avoid # the ndk r8c or later 'wordlist' errors. The outcome of this # under gnu/linux is the same as when using tools like # dos2unix, but the native python way is faster and will # work in non-unix systems. manifest_text = open(manifest, 'U').read() open(manifest, 'w').write(manifest_text) # In case the AM.xml read was big, free the memory del manifest_text p = FDroidPopen(cmd, cwd=os.path.join(root_dir, d)) if p.returncode != 0: raise BuildException( "NDK build failed for %s:%s" % (app['id'], thisbuild['version']), p.output) p = None # Build the release... if thisbuild['type'] == 'maven': logging.info("Building Maven project...") if '@' in thisbuild['maven']: maven_dir = os.path.join(root_dir, thisbuild['maven'].split('@', 1)[1]) else: maven_dir = root_dir mvncmd = [ config['mvn3'], '-Dandroid.sdk.path=' + config['sdk_path'], '-Dmaven.jar.sign.skip=true', '-Dmaven.test.skip=true', '-Dandroid.sign.debug=false', '-Dandroid.release=true', 'package' ] if thisbuild['target']: target = thisbuild["target"].split('-')[1] common.regsub_file(r'<platform>[0-9]*</platform>', r'<platform>%s</platform>' % target, os.path.join(root_dir, 'pom.xml')) if '@' in thisbuild['maven']: common.regsub_file(r'<platform>[0-9]*</platform>', r'<platform>%s</platform>' % target, os.path.join(maven_dir, 'pom.xml')) p = FDroidPopen(mvncmd, cwd=maven_dir) bindir = os.path.join(root_dir, 'target') elif thisbuild['type'] == 'kivy': logging.info("Building Kivy project...") spec = os.path.join(root_dir, 'buildozer.spec') if not os.path.exists(spec): raise BuildException( "Expected to find buildozer-compatible spec at {0}".format( spec)) defaults = { 'orientation': 'landscape', 'icon': '', 'permissions': '', 'android.api': "18" } bconfig = ConfigParser(defaults, allow_no_value=True) bconfig.read(spec) distdir = 'python-for-android/dist/fdroid' if os.path.exists(distdir): shutil.rmtree(distdir) modules = bconfig.get('app', 'requirements').split(',') cmd = 'ANDROIDSDK=' + config['sdk_path'] cmd += ' ANDROIDNDK=' + thisbuild['ndk_path'] cmd += ' ANDROIDNDKVER=' + thisbuild['ndk'] cmd += ' ANDROIDAPI=' + str(bconfig.get('app', 'android.api')) cmd += ' VIRTUALENV=virtualenv' cmd += ' ./distribute.sh' cmd += ' -m ' + "'" + ' '.join(modules) + "'" cmd += ' -d fdroid' p = subprocess.Popen(cmd, cwd='python-for-android', shell=True) if p.returncode != 0: raise BuildException("Distribute build failed") cid = bconfig.get('app', 'package.domain') + '.' + bconfig.get( 'app', 'package.name') if cid != app['id']: raise BuildException( "Package ID mismatch between metadata and spec") orientation = bconfig.get('app', 'orientation', 'landscape') if orientation == 'all': orientation = 'sensor' cmd = [ './build.py' '--dir', root_dir, '--name', bconfig.get('app', 'title'), '--package', app['id'], '--version', bconfig.get('app', 'version'), '--orientation', orientation ] perms = bconfig.get('app', 'permissions') for perm in perms.split(','): cmd.extend(['--permission', perm]) if config.get('app', 'fullscreen') == 0: cmd.append('--window') icon = bconfig.get('app', 'icon.filename') if icon: cmd.extend(['--icon', os.path.join(root_dir, icon)]) cmd.append('release') p = FDroidPopen(cmd, cwd=distdir) elif thisbuild['type'] == 'gradle': logging.info("Building Gradle project...") # Avoid having to use lintOptions.abortOnError false if thisbuild['gradlepluginver'] >= LooseVersion('0.7'): with open(os.path.join(root_dir, 'build.gradle'), "a") as f: f.write( "\nandroid { lintOptions { checkReleaseBuilds false } }\n") cmd = [config['gradle']] if thisbuild['gradleprops']: cmd += ['-P' + kv for kv in thisbuild['gradleprops']] cmd += gradletasks p = FDroidPopen(cmd, cwd=root_dir) elif thisbuild['type'] == 'ant': logging.info("Building Ant project...") cmd = ['ant'] if thisbuild['antcommands']: cmd += thisbuild['antcommands'] else: cmd += ['release'] p = FDroidPopen(cmd, cwd=root_dir) bindir = os.path.join(root_dir, 'bin') if p is not None and p.returncode != 0: raise BuildException( "Build failed for %s:%s" % (app['id'], thisbuild['version']), p.output) logging.info("Successfully built version " + thisbuild['version'] + ' of ' + app['id']) if thisbuild['type'] == 'maven': stdout_apk = '\n'.join([ line for line in p.output.splitlines() if any(a in line for a in ('.apk', '.ap_', '.jar')) ]) m = re.match(r".*^\[INFO\] .*apkbuilder.*/([^/]*)\.apk", stdout_apk, re.S | re.M) if not m: m = re.match( r".*^\[INFO\] Creating additional unsigned apk file .*/([^/]+)\.apk[^l]", stdout_apk, re.S | re.M) if not m: m = re.match( r'.*^\[INFO\] [^$]*aapt \[package,[^$]*' + bindir + r'/([^/]+)\.ap[_k][,\]]', stdout_apk, re.S | re.M) if not m: m = re.match( r".*^\[INFO\] Building jar: .*/" + bindir + r"/(.+)\.jar", stdout_apk, re.S | re.M) if not m: raise BuildException('Failed to find output') src = m.group(1) src = os.path.join(bindir, src) + '.apk' elif thisbuild['type'] == 'kivy': src = 'python-for-android/dist/default/bin/{0}-{1}-release.apk'.format( bconfig.get('app', 'title'), bconfig.get('app', 'version')) elif thisbuild['type'] == 'gradle': if thisbuild['gradlepluginver'] >= LooseVersion('0.11'): apks_dir = os.path.join(root_dir, 'build', 'outputs', 'apk') else: apks_dir = os.path.join(root_dir, 'build', 'apk') apks = glob.glob(os.path.join(apks_dir, '*-release-unsigned.apk')) if len(apks) > 1: raise BuildException( 'More than one resulting apks found in %s' % apks_dir, '\n'.join(apks)) if len(apks) < 1: raise BuildException('Failed to find gradle output in %s' % apks_dir) src = apks[0] elif thisbuild['type'] == 'ant': stdout_apk = '\n'.join( [line for line in p.output.splitlines() if '.apk' in line]) src = re.match(r".*^.*Creating (.+) for release.*$.*", stdout_apk, re.S | re.M).group(1) src = os.path.join(bindir, src) elif thisbuild['type'] == 'raw': src = os.path.join(root_dir, thisbuild['output']) src = os.path.normpath(src) # Make sure it's not debuggable... if common.isApkDebuggable(src, config): raise BuildException("APK is debuggable") # By way of a sanity check, make sure the version and version # code in our new apk match what we expect... logging.debug("Checking " + src) if not os.path.exists(src): raise BuildException("Unsigned apk is not at expected location of " + src) p = SdkToolsPopen(['aapt', 'dump', 'badging', src], output=False) vercode = None version = None foundid = None nativecode = None for line in p.output.splitlines(): if line.startswith("package:"): pat = re.compile(".*name='([a-zA-Z0-9._]*)'.*") m = pat.match(line) if m: foundid = m.group(1) pat = re.compile(".*versionCode='([0-9]*)'.*") m = pat.match(line) if m: vercode = m.group(1) pat = re.compile(".*versionName='([^']*)'.*") m = pat.match(line) if m: version = m.group(1) elif line.startswith("native-code:"): nativecode = line[12:] # Ignore empty strings or any kind of space/newline chars that we don't # care about if nativecode is not None: nativecode = nativecode.strip() nativecode = None if not nativecode else nativecode if thisbuild['buildjni'] and thisbuild['buildjni'] != ['no']: if nativecode is None: raise BuildException( "Native code should have been built but none was packaged") if thisbuild['novcheck']: vercode = thisbuild['vercode'] version = thisbuild['version'] if not version or not vercode: raise BuildException( "Could not find version information in build in output") if not foundid: raise BuildException("Could not find package ID in output") if foundid != app['id']: raise BuildException("Wrong package ID - build " + foundid + " but expected " + app['id']) # Some apps (e.g. Timeriffic) have had the bonkers idea of # including the entire changelog in the version number. Remove # it so we can compare. (TODO: might be better to remove it # before we compile, in fact) index = version.find(" //") if index != -1: version = version[:index] if (version != thisbuild['version'] or vercode != thisbuild['vercode']): raise BuildException(("Unexpected version/version code in output;" " APK: '%s' / '%s', " " Expected: '%s' / '%s'") % (version, str(vercode), thisbuild['version'], str(thisbuild['vercode']))) # Add information for 'fdroid verify' to be able to reproduce the build # environment. if onserver: metadir = os.path.join(tmp_dir, 'META-INF') if not os.path.exists(metadir): os.mkdir(metadir) homedir = os.path.expanduser('~') for fn in ['buildserverid', 'fdroidserverid']: shutil.copyfile(os.path.join(homedir, fn), os.path.join(metadir, fn)) subprocess.call( ['jar', 'uf', os.path.abspath(src), 'META-INF/' + fn], cwd=tmp_dir) # Copy the unsigned apk to our destination directory for further # processing (by publish.py)... dest = os.path.join(output_dir, common.getapkname(app, thisbuild)) shutil.copyfile(src, dest) # Move the source tarball into the output directory... if output_dir != tmp_dir and not options.notarball: shutil.move(os.path.join(tmp_dir, tarname), os.path.join(output_dir, tarname))
def build_local(app, build, vcs, build_dir, output_dir, srclib_dir, extlib_dir, tmp_dir, force, onserver, refresh): """Do a build locally.""" ndk_path = build.ndk_path() if build.buildjni and build.buildjni != ['no']: if not ndk_path: logging.critical("Android NDK version '%s' could not be found!" % build.ndk or 'r10e') logging.critical("Configured versions:") for k, v in config['ndk_paths'].iteritems(): if k.endswith("_orig"): continue logging.critical(" %s: %s" % (k, v)) sys.exit(3) elif not os.path.isdir(ndk_path): logging.critical("Android NDK '%s' is not a directory!" % ndk_path) sys.exit(3) # Set up environment vars that depend on each build for n in ['ANDROID_NDK', 'NDK', 'ANDROID_NDK_HOME']: common.env[n] = ndk_path common.reset_env_path() # Set up the current NDK to the PATH common.add_to_env_path(ndk_path) # Prepare the source code... root_dir, srclibpaths = common.prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver, refresh) # We need to clean via the build tool in case the binary dirs are # different from the default ones p = None gradletasks = [] bmethod = build.build_method() if bmethod == 'maven': logging.info("Cleaning Maven project...") cmd = [config['mvn3'], 'clean', '-Dandroid.sdk.path=' + config['sdk_path']] if '@' in build.maven: maven_dir = os.path.join(root_dir, build.maven.split('@', 1)[1]) maven_dir = os.path.normpath(maven_dir) else: maven_dir = root_dir p = FDroidPopen(cmd, cwd=maven_dir) elif bmethod == 'gradle': logging.info("Cleaning Gradle project...") if build.preassemble: gradletasks += build.preassemble flavours = build.gradle if flavours == ['yes']: flavours = [] flavours_cmd = ''.join([capitalize_intact(f) for f in flavours]) gradletasks += ['assemble' + flavours_cmd + 'Release'] adapt_gradle(build_dir) for name, number, libpath in srclibpaths: adapt_gradle(libpath) cmd = [config['gradle']] if build.gradleprops: cmd += ['-P' + kv for kv in build.gradleprops] cmd += ['clean'] p = FDroidPopen(cmd, cwd=root_dir) elif bmethod == 'kivy': pass elif bmethod == 'ant': logging.info("Cleaning Ant project...") p = FDroidPopen(['ant', 'clean'], cwd=root_dir) if p is not None and p.returncode != 0: raise BuildException("Error cleaning %s:%s" % (app.id, build.version), p.output) for root, dirs, files in os.walk(build_dir): def del_dirs(dl): for d in dl: if d in dirs: shutil.rmtree(os.path.join(root, d)) def del_files(fl): for f in fl: if f in files: os.remove(os.path.join(root, f)) if 'build.gradle' in files: # Even when running clean, gradle stores task/artifact caches in # .gradle/ as binary files. To avoid overcomplicating the scanner, # manually delete them, just like `gradle clean` should have removed # the build/ dirs. del_dirs(['build', '.gradle']) del_files(['gradlew', 'gradlew.bat']) if 'pom.xml' in files: del_dirs(['target']) if any(f in files for f in ['ant.properties', 'project.properties', 'build.xml']): del_dirs(['bin', 'gen']) if 'jni' in dirs: del_dirs(['obj']) if options.skipscan: if build.scandelete: raise BuildException("Refusing to skip source scan since scandelete is present") else: # Scan before building... logging.info("Scanning source for common problems...") count = scanner.scan_source(build_dir, root_dir, build) if count > 0: if force: logging.warn('Scanner found %d problems' % count) else: raise BuildException("Can't build due to %d errors while scanning" % count) if not options.notarball: # Build the source tarball right before we build the release... logging.info("Creating source tarball...") tarname = common.getsrcname(app, build) tarball = tarfile.open(os.path.join(tmp_dir, tarname), "w:gz") def tarexc(f): return any(f.endswith(s) for s in ['.svn', '.git', '.hg', '.bzr']) tarball.add(build_dir, tarname, exclude=tarexc) tarball.close() # Run a build command if one is required... if build.build: logging.info("Running 'build' commands in %s" % root_dir) cmd = common.replace_config_vars(build.build, build) # Substitute source library paths into commands... for name, number, libpath in srclibpaths: libpath = os.path.relpath(libpath, root_dir) cmd = cmd.replace('$$' + name + '$$', libpath) p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir) if p.returncode != 0: raise BuildException("Error running build command for %s:%s" % (app.id, build.version), p.output) # Build native stuff if required... if build.buildjni and build.buildjni != ['no']: logging.info("Building the native code") jni_components = build.buildjni if jni_components == ['yes']: jni_components = [''] cmd = [os.path.join(ndk_path, "ndk-build"), "-j1"] for d in jni_components: if d: logging.info("Building native code in '%s'" % d) else: logging.info("Building native code in the main project") manifest = os.path.join(root_dir, d, 'AndroidManifest.xml') if os.path.exists(manifest): # Read and write the whole AM.xml to fix newlines and avoid # the ndk r8c or later 'wordlist' errors. The outcome of this # under gnu/linux is the same as when using tools like # dos2unix, but the native python way is faster and will # work in non-unix systems. manifest_text = open(manifest, 'U').read() open(manifest, 'w').write(manifest_text) # In case the AM.xml read was big, free the memory del manifest_text p = FDroidPopen(cmd, cwd=os.path.join(root_dir, d)) if p.returncode != 0: raise BuildException("NDK build failed for %s:%s" % (app.id, build.version), p.output) p = None # Build the release... if bmethod == 'maven': logging.info("Building Maven project...") if '@' in build.maven: maven_dir = os.path.join(root_dir, build.maven.split('@', 1)[1]) else: maven_dir = root_dir mvncmd = [config['mvn3'], '-Dandroid.sdk.path=' + config['sdk_path'], '-Dmaven.jar.sign.skip=true', '-Dmaven.test.skip=true', '-Dandroid.sign.debug=false', '-Dandroid.release=true', 'package'] if build.target: target = build.target.split('-')[1] common.regsub_file(r'<platform>[0-9]*</platform>', r'<platform>%s</platform>' % target, os.path.join(root_dir, 'pom.xml')) if '@' in build.maven: common.regsub_file(r'<platform>[0-9]*</platform>', r'<platform>%s</platform>' % target, os.path.join(maven_dir, 'pom.xml')) p = FDroidPopen(mvncmd, cwd=maven_dir) bindir = os.path.join(root_dir, 'target') elif bmethod == 'kivy': logging.info("Building Kivy project...") spec = os.path.join(root_dir, 'buildozer.spec') if not os.path.exists(spec): raise BuildException("Expected to find buildozer-compatible spec at {0}" .format(spec)) defaults = {'orientation': 'landscape', 'icon': '', 'permissions': '', 'android.api': "18"} bconfig = ConfigParser(defaults, allow_no_value=True) bconfig.read(spec) distdir = os.path.join('python-for-android', 'dist', 'fdroid') if os.path.exists(distdir): shutil.rmtree(distdir) modules = bconfig.get('app', 'requirements').split(',') cmd = 'ANDROIDSDK=' + config['sdk_path'] cmd += ' ANDROIDNDK=' + ndk_path cmd += ' ANDROIDNDKVER=' + build.ndk cmd += ' ANDROIDAPI=' + str(bconfig.get('app', 'android.api')) cmd += ' VIRTUALENV=virtualenv' cmd += ' ./distribute.sh' cmd += ' -m ' + "'" + ' '.join(modules) + "'" cmd += ' -d fdroid' p = subprocess.Popen(cmd, cwd='python-for-android', shell=True) if p.returncode != 0: raise BuildException("Distribute build failed") cid = bconfig.get('app', 'package.domain') + '.' + bconfig.get('app', 'package.name') if cid != app.id: raise BuildException("Package ID mismatch between metadata and spec") orientation = bconfig.get('app', 'orientation', 'landscape') if orientation == 'all': orientation = 'sensor' cmd = ['./build.py' '--dir', root_dir, '--name', bconfig.get('app', 'title'), '--package', app.id, '--version', bconfig.get('app', 'version'), '--orientation', orientation ] perms = bconfig.get('app', 'permissions') for perm in perms.split(','): cmd.extend(['--permission', perm]) if config.get('app', 'fullscreen') == 0: cmd.append('--window') icon = bconfig.get('app', 'icon.filename') if icon: cmd.extend(['--icon', os.path.join(root_dir, icon)]) cmd.append('release') p = FDroidPopen(cmd, cwd=distdir) elif bmethod == 'gradle': logging.info("Building Gradle project...") cmd = [config['gradle']] if build.gradleprops: cmd += ['-P' + kv for kv in build.gradleprops] cmd += gradletasks p = FDroidPopen(cmd, cwd=root_dir) elif bmethod == 'ant': logging.info("Building Ant project...") cmd = ['ant'] if build.antcommands: cmd += build.antcommands else: cmd += ['release'] p = FDroidPopen(cmd, cwd=root_dir) bindir = os.path.join(root_dir, 'bin') if p is not None and p.returncode != 0: raise BuildException("Build failed for %s:%s" % (app.id, build.version), p.output) logging.info("Successfully built version " + build.version + ' of ' + app.id) omethod = build.output_method() if omethod == 'maven': stdout_apk = '\n'.join([ line for line in p.output.splitlines() if any( a in line for a in ('.apk', '.ap_', '.jar'))]) m = re.match(r".*^\[INFO\] .*apkbuilder.*/([^/]*)\.apk", stdout_apk, re.S | re.M) if not m: m = re.match(r".*^\[INFO\] Creating additional unsigned apk file .*/([^/]+)\.apk[^l]", stdout_apk, re.S | re.M) if not m: m = re.match(r'.*^\[INFO\] [^$]*aapt \[package,[^$]*' + bindir + r'/([^/]+)\.ap[_k][,\]]', stdout_apk, re.S | re.M) if not m: m = re.match(r".*^\[INFO\] Building jar: .*/" + bindir + r"/(.+)\.jar", stdout_apk, re.S | re.M) if not m: raise BuildException('Failed to find output') src = m.group(1) src = os.path.join(bindir, src) + '.apk' elif omethod == 'kivy': src = os.path.join('python-for-android', 'dist', 'default', 'bin', '{0}-{1}-release.apk'.format( bconfig.get('app', 'title'), bconfig.get('app', 'version'))) elif omethod == 'gradle': src = None for apks_dir in [ os.path.join(root_dir, 'build', 'outputs', 'apk'), os.path.join(root_dir, 'build', 'apk'), ]: for apkglob in ['*-release-unsigned.apk', '*-unsigned.apk', '*.apk']: apks = glob.glob(os.path.join(apks_dir, apkglob)) if len(apks) > 1: raise BuildException('More than one resulting apks found in %s' % apks_dir, '\n'.join(apks)) if len(apks) == 1: src = apks[0] break if src is not None: break if src is None: raise BuildException('Failed to find any output apks') elif omethod == 'ant': stdout_apk = '\n'.join([ line for line in p.output.splitlines() if '.apk' in line]) src = re.match(r".*^.*Creating (.+) for release.*$.*", stdout_apk, re.S | re.M).group(1) src = os.path.join(bindir, src) elif omethod == 'raw': globpath = os.path.join(root_dir, build.output) apks = glob.glob(globpath) if len(apks) > 1: raise BuildException('Multiple apks match %s' % globpath, '\n'.join(apks)) if len(apks) < 1: raise BuildException('No apks match %s' % globpath) src = os.path.normpath(apks[0]) # Make sure it's not debuggable... if common.isApkDebuggable(src, config): raise BuildException("APK is debuggable") # By way of a sanity check, make sure the version and version # code in our new apk match what we expect... logging.debug("Checking " + src) if not os.path.exists(src): raise BuildException("Unsigned apk is not at expected location of " + src) p = SdkToolsPopen(['aapt', 'dump', 'badging', src], output=False) vercode = None version = None foundid = None nativecode = None for line in p.output.splitlines(): if line.startswith("package:"): pat = re.compile(".*name='([a-zA-Z0-9._]*)'.*") m = pat.match(line) if m: foundid = m.group(1) pat = re.compile(".*versionCode='([0-9]*)'.*") m = pat.match(line) if m: vercode = m.group(1) pat = re.compile(".*versionName='([^']*)'.*") m = pat.match(line) if m: version = m.group(1) elif line.startswith("native-code:"): nativecode = line[12:] # Ignore empty strings or any kind of space/newline chars that we don't # care about if nativecode is not None: nativecode = nativecode.strip() nativecode = None if not nativecode else nativecode if build.buildjni and build.buildjni != ['no']: if nativecode is None: raise BuildException("Native code should have been built but none was packaged") if build.novcheck: vercode = build.vercode version = build.version if not version or not vercode: raise BuildException("Could not find version information in build in output") if not foundid: raise BuildException("Could not find package ID in output") if foundid != app.id: raise BuildException("Wrong package ID - build " + foundid + " but expected " + app.id) # Some apps (e.g. Timeriffic) have had the bonkers idea of # including the entire changelog in the version number. Remove # it so we can compare. (TODO: might be better to remove it # before we compile, in fact) index = version.find(" //") if index != -1: version = version[:index] if (version != build.version or vercode != build.vercode): raise BuildException(("Unexpected version/version code in output;" " APK: '%s' / '%s', " " Expected: '%s' / '%s'") % (version, str(vercode), build.version, str(build.vercode)) ) # Add information for 'fdroid verify' to be able to reproduce the build # environment. if onserver: metadir = os.path.join(tmp_dir, 'META-INF') if not os.path.exists(metadir): os.mkdir(metadir) homedir = os.path.expanduser('~') for fn in ['buildserverid', 'fdroidserverid']: shutil.copyfile(os.path.join(homedir, fn), os.path.join(metadir, fn)) subprocess.call(['jar', 'uf', os.path.abspath(src), 'META-INF/' + fn], cwd=tmp_dir) # Copy the unsigned apk to our destination directory for further # processing (by publish.py)... dest = os.path.join(output_dir, common.getapkname(app, build)) shutil.copyfile(src, dest) # Move the source tarball into the output directory... if output_dir != tmp_dir and not options.notarball: shutil.move(os.path.join(tmp_dir, tarname), os.path.join(output_dir, tarname))