mirror of
				https://github.com/f-droid/fdroidserver.git
				synced 2025-11-04 06:30:27 +03:00 
			
		
		
		
	Merge branch 'buildbot-subcommands-prepare_source-build_local_run-make_src_tar_gz' into 'master'
more subcommands: prepare_source, build_local_run, install_ndk, make_source_tarball See merge request fdroid/fdroidserver!1712
This commit is contained in:
		
						commit
						a9f4467661
					
				
					 6 changed files with 1049 additions and 0 deletions
				
			
		| 
						 | 
				
			
			@ -65,8 +65,12 @@ COMMANDS = OrderedDict([
 | 
			
		|||
# interactively because they rely on the presense of a VM/container,
 | 
			
		||||
# modify the local environment, or even run things as root.
 | 
			
		||||
COMMANDS_INTERNAL = [
 | 
			
		||||
    "build_local_run",
 | 
			
		||||
    "destroy",
 | 
			
		||||
    "exec",
 | 
			
		||||
    "install_ndk",
 | 
			
		||||
    "make_source_tarball",
 | 
			
		||||
    "prepare_source",
 | 
			
		||||
    "pull",
 | 
			
		||||
    "push",
 | 
			
		||||
    "schedule_verify",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										757
									
								
								fdroidserver/build_local_run.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										757
									
								
								fdroidserver/build_local_run.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,757 @@
 | 
			
		|||
#!/usr/bin/env python3
 | 
			
		||||
#
 | 
			
		||||
# build_local_run.py - part of the F-Droid server tools
 | 
			
		||||
# Copyright (C) 2017-2025 Michael Pöhn <michael@poehn.at>
 | 
			
		||||
# Copyright (C) 2021-2025 linsui <2873532-linsui@users.noreply.gitlab.com>
 | 
			
		||||
# Copyright (C) 2021-2025 Jochen Sprickerhof <git@jochen.sprickerhof.de>
 | 
			
		||||
# Copyright (C) 2014-2025 Hans-Christoph Steiner <hans@eds.org>
 | 
			
		||||
# Copyright (C) 2024 g0t mi1k <have.you.g0tmi1k@gmail.com>
 | 
			
		||||
# Copyright (C) 2024 Gregor Düster <git@gdstr.eu>
 | 
			
		||||
# Copyright (C) 2023 cvzi <cuzi@openmail.cc>
 | 
			
		||||
# Copyright (C) 2023 Jason A. Donenfeld <Jason@zx2c4.com>
 | 
			
		||||
# Copyright (C) 2023 FestplattenSchnitzel <festplatte.schnitzel@posteo.de>
 | 
			
		||||
# Copyright (C) 2022 proletarius101 <proletarius101@protonmail.com>
 | 
			
		||||
# Copyright (C) 2021 Benedikt Brückmann <64bit+git@posteo.de>
 | 
			
		||||
# Copyright (C) 2021 Felix C. Stegerman <flx@obfusk.net>
 | 
			
		||||
# Copyright (C) 2017-2021 relan <email@hidden>
 | 
			
		||||
# Copyright (C) 2017-2020 Marcus Hoffmann <bubu@bubu1.eu>
 | 
			
		||||
# Copyright (C) 2016, 2017, 2020 mimi89999 <michel@lebihan.pl>
 | 
			
		||||
# Copyright (C) 2019 Michael von Glasow <michael@vonglasow.com>
 | 
			
		||||
# Copyright (C) 2018 csagan5 <32685696+csagan5@users.noreply.github.com>
 | 
			
		||||
# Copyright (C) 2018 Areeb Jamal <jamal.areeb@gmail.com>
 | 
			
		||||
# Copyright (C) 2017-2018 Torsten Grote <t@grobox.de>
 | 
			
		||||
# Copyright (C) 2017 thez3ro <io@thezero.org>
 | 
			
		||||
# Copyright (C) 2017 lb@lb520 <lb@lb520>
 | 
			
		||||
# Copyright (C) 2017 Jan Berkel <jan@berkel.fr>
 | 
			
		||||
# Copyright (C) 2013-2016 Daniel Martí <mvdan@mvdan.cc>
 | 
			
		||||
# Copyright (C) 2010-2016 Ciaran Gultnieks, ciaran@ciarang.com
 | 
			
		||||
# Copyright (C) 2016 Kevin C. Krinke <kevin@krinke.ca>
 | 
			
		||||
# Copyright (C) 2015 أحمد المحمودي (Ahmed El-Mahmoudy) <aelmahmoudy@users.sourceforge.net>
 | 
			
		||||
# Copyright (C) 2015 nero-tux <neroburner@hotmail.de>
 | 
			
		||||
# Copyright (C) 2015 Rancor <fisch.666@gmx.de>
 | 
			
		||||
# Copyright (C) 2015 Lode Hoste <zillode@zillode.be>
 | 
			
		||||
# Copyright (C) 2013 Simon Josefsson <simon@josefsson.org>
 | 
			
		||||
# Copyright (C) 2013 Frans Gifford <frans.gifford@cs.ox.ac.uk>
 | 
			
		||||
# Copyright (C) 2013 Christopher <christopher@gittner.org>
 | 
			
		||||
# Copyright (C) 2012-2013 Paul Sokolovsky <pfalcon@users.sourceforge.net>
 | 
			
		||||
# Copyright (C) 2012 Tias Guns <tias@ulyssis.org>
 | 
			
		||||
#
 | 
			
		||||
# This program is free software: you can redistribute it and/or modify
 | 
			
		||||
# it under the terms of the GNU Affero General Public License as published by
 | 
			
		||||
# the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
# (at your option) any later version.
 | 
			
		||||
#
 | 
			
		||||
# This program is distributed in the hope that it will be useful,
 | 
			
		||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
# GNU Affero General Public License for more details.
 | 
			
		||||
#
 | 
			
		||||
# You should have received a copy of the GNU Affero General Public License
 | 
			
		||||
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
"""Build one specific in app, assuming all required build tools are installed and configured."""
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
import sys
 | 
			
		||||
import glob
 | 
			
		||||
import shutil
 | 
			
		||||
import logging
 | 
			
		||||
import pathlib
 | 
			
		||||
import argparse
 | 
			
		||||
import traceback
 | 
			
		||||
 | 
			
		||||
from fdroidserver import common, exception, metadata
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def rlimit_check(apps_count=1):
 | 
			
		||||
    """Make sure Linux is configured to allow for enough simultaneously open files.
 | 
			
		||||
 | 
			
		||||
    TODO: check if this is obsolete
 | 
			
		||||
 | 
			
		||||
    Parameters
 | 
			
		||||
    ----------
 | 
			
		||||
    apps_count
 | 
			
		||||
        In the past this used to be `len(apps)` In this context we're
 | 
			
		||||
        always buidling just one app so this is always 1
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        import resource  # not available on Windows
 | 
			
		||||
 | 
			
		||||
        soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
 | 
			
		||||
        if apps_count > soft:
 | 
			
		||||
            try:
 | 
			
		||||
                soft = apps_count * 2
 | 
			
		||||
                if soft > hard:
 | 
			
		||||
                    soft = hard
 | 
			
		||||
                resource.setrlimit(resource.RLIMIT_NOFILE, (soft, hard))
 | 
			
		||||
                logging.debug(f'Set open file limit to {soft}')
 | 
			
		||||
            except (OSError, ValueError) as e:
 | 
			
		||||
                logging.warning('Setting open file limit failed: ' + str(e))
 | 
			
		||||
    except ImportError:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_build_root_dir(app, build):
 | 
			
		||||
    if build.subdir:
 | 
			
		||||
        return os.path.join(common.get_build_dir(app), build.subdir)
 | 
			
		||||
    return common.get_build_dir(app)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def transform_first_char(string, method):
 | 
			
		||||
    """Use method() on the first character of string."""
 | 
			
		||||
    if len(string) == 0:
 | 
			
		||||
        return string
 | 
			
		||||
    if len(string) == 1:
 | 
			
		||||
        return method(string)
 | 
			
		||||
    return method(string[0]) + string[1:]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_flavours_cmd(build):
 | 
			
		||||
    """Get flavor string, preformatted for gradle cli.
 | 
			
		||||
 | 
			
		||||
    Reads build flavors form metadata if any and reformats and concatenates
 | 
			
		||||
    them to be ready for use as CLI arguments to gradle. This will treat the
 | 
			
		||||
    vlue 'yes' as if there were not particular build flavor selected.
 | 
			
		||||
 | 
			
		||||
    Parameters
 | 
			
		||||
    ----------
 | 
			
		||||
    build
 | 
			
		||||
        The metadata build entry you'd like to read flavors from
 | 
			
		||||
 | 
			
		||||
    Returns
 | 
			
		||||
    -------
 | 
			
		||||
    A string containing the build flavor for this build. If it's the default
 | 
			
		||||
    flavor ("yes" in metadata) this returns an empty string. Returns None if
 | 
			
		||||
    it's not a gradle build.
 | 
			
		||||
    """
 | 
			
		||||
    flavours = build.gradle
 | 
			
		||||
 | 
			
		||||
    if flavours == ['yes']:
 | 
			
		||||
        flavours = []
 | 
			
		||||
 | 
			
		||||
    flavours_cmd = ''.join([transform_first_char(flav, str.upper) for flav in flavours])
 | 
			
		||||
 | 
			
		||||
    return flavours_cmd
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def init_build(app, build, config):
 | 
			
		||||
    root_dir = get_build_root_dir(app, build)
 | 
			
		||||
 | 
			
		||||
    p = None
 | 
			
		||||
    gradletasks = []
 | 
			
		||||
 | 
			
		||||
    # We need to clean via the build tool in case the binary dirs are
 | 
			
		||||
    # different from the default ones
 | 
			
		||||
 | 
			
		||||
    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 = common.FDroidPopen(cmd, cwd=maven_dir)
 | 
			
		||||
 | 
			
		||||
    elif bmethod == 'gradle':
 | 
			
		||||
        logging.info("Cleaning Gradle project...")
 | 
			
		||||
 | 
			
		||||
        if build.preassemble:
 | 
			
		||||
            gradletasks += build.preassemble
 | 
			
		||||
 | 
			
		||||
        flavours_cmd = get_flavours_cmd(build)
 | 
			
		||||
 | 
			
		||||
        gradletasks += ['assemble' + flavours_cmd + 'Release']
 | 
			
		||||
 | 
			
		||||
        cmd = [config['gradle']]
 | 
			
		||||
        if build.gradleprops:
 | 
			
		||||
            cmd += ['-P' + kv for kv in build.gradleprops]
 | 
			
		||||
 | 
			
		||||
        cmd += ['clean']
 | 
			
		||||
        p = common.FDroidPopen(
 | 
			
		||||
            cmd,
 | 
			
		||||
            cwd=root_dir,
 | 
			
		||||
            envs={
 | 
			
		||||
                "GRADLE_VERSION_DIR": config['gradle_version_dir'],
 | 
			
		||||
                "CACHEDIR": config['cachedir'],
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    elif bmethod == 'ant':
 | 
			
		||||
        logging.info("Cleaning Ant project...")
 | 
			
		||||
        p = common.FDroidPopen(['ant', 'clean'], cwd=root_dir)
 | 
			
		||||
 | 
			
		||||
    if p is not None and p.returncode != 0:
 | 
			
		||||
        raise exception.BuildException(
 | 
			
		||||
            "Error cleaning %s:%s" % (app.id, build.versionCode), p.output
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    return gradletasks
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def sanitize_build_dir(app):
 | 
			
		||||
    """Delete build output directories.
 | 
			
		||||
 | 
			
		||||
    This function deletes the default build/binary/target/... output
 | 
			
		||||
    directories for follwoing build tools: gradle, maven, ant, jni. It also
 | 
			
		||||
    deletes gradle-wrapper if present. It just uses parths, hardcoded here,
 | 
			
		||||
    it doesn't call and build system clean routines.
 | 
			
		||||
 | 
			
		||||
    Parameters
 | 
			
		||||
    ----------
 | 
			
		||||
    app
 | 
			
		||||
        The metadata of the app to sanitize
 | 
			
		||||
    """
 | 
			
		||||
    build_dir = common.get_build_dir(app)
 | 
			
		||||
    for root, dirs, files in os.walk(build_dir):
 | 
			
		||||
 | 
			
		||||
        def del_dirs(dl):
 | 
			
		||||
            for d in dl:
 | 
			
		||||
                shutil.rmtree(os.path.join(root, d), ignore_errors=True)
 | 
			
		||||
 | 
			
		||||
        def del_files(fl):
 | 
			
		||||
            for f in fl:
 | 
			
		||||
                if f in files:
 | 
			
		||||
                    os.remove(os.path.join(root, f))
 | 
			
		||||
 | 
			
		||||
        if any(
 | 
			
		||||
            f in files
 | 
			
		||||
            for f in [
 | 
			
		||||
                'build.gradle',
 | 
			
		||||
                'build.gradle.kts',
 | 
			
		||||
                'settings.gradle',
 | 
			
		||||
                'settings.gradle.kts',
 | 
			
		||||
            ]
 | 
			
		||||
        ):
 | 
			
		||||
            # 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(
 | 
			
		||||
                [
 | 
			
		||||
                    os.path.join('build', 'android-profile'),
 | 
			
		||||
                    os.path.join('build', 'generated'),
 | 
			
		||||
                    os.path.join('build', 'intermediates'),
 | 
			
		||||
                    os.path.join('build', 'outputs'),
 | 
			
		||||
                    os.path.join('build', 'reports'),
 | 
			
		||||
                    os.path.join('build', 'tmp'),
 | 
			
		||||
                    os.path.join('buildSrc', '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'])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def execute_build_commands(app, build):
 | 
			
		||||
    """Execute `bulid` commands if present in metadata.
 | 
			
		||||
 | 
			
		||||
    see: https://f-droid.org/docs/Build_Metadata_Reference/#build_build
 | 
			
		||||
 | 
			
		||||
    Parameters
 | 
			
		||||
    ----------
 | 
			
		||||
    app
 | 
			
		||||
        metadata app object
 | 
			
		||||
    build
 | 
			
		||||
        metadata build object
 | 
			
		||||
    """
 | 
			
		||||
    root_dir = get_build_root_dir(app, build)
 | 
			
		||||
    srclibpaths = get_srclibpaths(app, build)
 | 
			
		||||
 | 
			
		||||
    if build.build:
 | 
			
		||||
        logging.info("Running 'build' commands in %s" % root_dir)
 | 
			
		||||
        cmd = common.replace_config_vars("; ".join(build.build), build)
 | 
			
		||||
 | 
			
		||||
        # Substitute source library paths into commands...
 | 
			
		||||
        for name, number, libpath in srclibpaths:
 | 
			
		||||
            cmd = cmd.replace('$$' + name + '$$', os.path.join(os.getcwd(), libpath))
 | 
			
		||||
 | 
			
		||||
        p = common.FDroidPopen(
 | 
			
		||||
            ['bash', '-e', '-u', '-o', 'pipefail', '-x', '-c', cmd], cwd=root_dir
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if p.returncode != 0:
 | 
			
		||||
            raise exception.BuildException(
 | 
			
		||||
                f"Error running build command for {app.id}:{build.versionCode}",
 | 
			
		||||
                p.output,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_srclibpaths(app, build):
 | 
			
		||||
    """Get srclibpaths list of tuples.
 | 
			
		||||
 | 
			
		||||
    This will just assemble the srclibpaths list of tuples, it won't fetch
 | 
			
		||||
    or checkout any source code, identical to return value of
 | 
			
		||||
    common.prepare_souce().
 | 
			
		||||
 | 
			
		||||
    Parameters
 | 
			
		||||
    ----------
 | 
			
		||||
    app
 | 
			
		||||
        metadata app object
 | 
			
		||||
    build
 | 
			
		||||
        metadata build object
 | 
			
		||||
 | 
			
		||||
    Returns
 | 
			
		||||
    -------
 | 
			
		||||
    List of srclibpath tuples
 | 
			
		||||
    """
 | 
			
		||||
    vcs, _ = common.setup_vcs(app)
 | 
			
		||||
 | 
			
		||||
    srclibpaths = []
 | 
			
		||||
    if build.srclibs:
 | 
			
		||||
        logging.info("Collecting source libraries")
 | 
			
		||||
        for lib in build.srclibs:
 | 
			
		||||
            srclibpaths.append(
 | 
			
		||||
                common.getsrclib(
 | 
			
		||||
                    lib,
 | 
			
		||||
                    "./build/srclib",
 | 
			
		||||
                    build=build,
 | 
			
		||||
                    prepare=False,
 | 
			
		||||
                    refresh=False,
 | 
			
		||||
                    preponly=True,
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    basesrclib = vcs.getsrclib()
 | 
			
		||||
    if basesrclib:
 | 
			
		||||
        srclibpaths.append(basesrclib)
 | 
			
		||||
 | 
			
		||||
    return srclibpaths
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def execute_buildjni_commands(app, build):
 | 
			
		||||
    root_dir = get_build_root_dir(app, build)
 | 
			
		||||
    ndk_path = build.ndk_path()
 | 
			
		||||
    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 = common.FDroidPopen(cmd, cwd=os.path.join(root_dir, d))
 | 
			
		||||
            if p.returncode != 0:
 | 
			
		||||
                raise exception.BuildException(
 | 
			
		||||
                    "NDK build failed for %s:%s" % (app.id, build.versionName), p.output
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def execute_build(app, build, config, gradletasks):
 | 
			
		||||
    root_dir = get_build_root_dir(app, build)
 | 
			
		||||
 | 
			
		||||
    p = None
 | 
			
		||||
    bindir = None
 | 
			
		||||
    bmethod = build.build_method()
 | 
			
		||||
    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 = common.FDroidPopen(mvncmd, cwd=maven_dir)
 | 
			
		||||
 | 
			
		||||
        bindir = os.path.join(root_dir, 'target')
 | 
			
		||||
 | 
			
		||||
    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 = common.FDroidPopen(
 | 
			
		||||
            cmd,
 | 
			
		||||
            cwd=root_dir,
 | 
			
		||||
            envs={
 | 
			
		||||
                "GRADLE_VERSION_DIR": config['gradle_version_dir'],
 | 
			
		||||
                "CACHEDIR": config['cachedir'],
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    elif bmethod == 'ant':
 | 
			
		||||
        logging.info("Building Ant project...")
 | 
			
		||||
        cmd = ['ant']
 | 
			
		||||
        if build.antcommands:
 | 
			
		||||
            cmd += build.antcommands
 | 
			
		||||
        else:
 | 
			
		||||
            cmd += ['release']
 | 
			
		||||
        p = common.FDroidPopen(cmd, cwd=root_dir)
 | 
			
		||||
 | 
			
		||||
        bindir = os.path.join(root_dir, 'bin')
 | 
			
		||||
 | 
			
		||||
    return p, bindir
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def collect_build_output(app, build, p, bindir):
 | 
			
		||||
    root_dir = get_build_root_dir(app, build)
 | 
			
		||||
 | 
			
		||||
    omethod = build.output_method()
 | 
			
		||||
    src = None
 | 
			
		||||
    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 exception.BuildException('Failed to find output')
 | 
			
		||||
        src = m.group(1)
 | 
			
		||||
        src = os.path.join(bindir, src) + '.apk'
 | 
			
		||||
 | 
			
		||||
    elif omethod == 'gradle':
 | 
			
		||||
        src = None
 | 
			
		||||
        apk_dirs = [
 | 
			
		||||
            # gradle plugin >= 3.0
 | 
			
		||||
            os.path.join(root_dir, 'build', 'outputs', 'apk', 'release'),
 | 
			
		||||
            # gradle plugin < 3.0 and >= 0.11
 | 
			
		||||
            os.path.join(root_dir, 'build', 'outputs', 'apk'),
 | 
			
		||||
            # really old path
 | 
			
		||||
            os.path.join(root_dir, 'build', 'apk'),
 | 
			
		||||
        ]
 | 
			
		||||
        # If we build with gradle flavours with gradle plugin >= 3.0 the APK will be in
 | 
			
		||||
        # a subdirectory corresponding to the flavour command used, but with different
 | 
			
		||||
        # capitalization.
 | 
			
		||||
        flavours_cmd = get_flavours_cmd(build)
 | 
			
		||||
        if flavours_cmd:
 | 
			
		||||
            apk_dirs.append(
 | 
			
		||||
                os.path.join(
 | 
			
		||||
                    root_dir,
 | 
			
		||||
                    'build',
 | 
			
		||||
                    'outputs',
 | 
			
		||||
                    'apk',
 | 
			
		||||
                    transform_first_char(flavours_cmd, str.lower),
 | 
			
		||||
                    'release',
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        for apks_dir in apk_dirs:
 | 
			
		||||
            for apkglob in ['*-release-unsigned.apk', '*-unsigned.apk', '*.apk']:
 | 
			
		||||
                apks = glob.glob(os.path.join(apks_dir, apkglob))
 | 
			
		||||
 | 
			
		||||
                if len(apks) > 1:
 | 
			
		||||
                    raise exception.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 exception.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':
 | 
			
		||||
        output_path = common.replace_build_vars(build.output, build)
 | 
			
		||||
        globpath = os.path.join(root_dir, output_path)
 | 
			
		||||
        apks = glob.glob(globpath)
 | 
			
		||||
        if len(apks) > 1:
 | 
			
		||||
            raise exception.BuildException(
 | 
			
		||||
                'Multiple apks match %s' % globpath, '\n'.join(apks)
 | 
			
		||||
            )
 | 
			
		||||
        if len(apks) < 1:
 | 
			
		||||
            raise exception.BuildException('No apks match %s' % globpath)
 | 
			
		||||
        src = os.path.normpath(apks[0])
 | 
			
		||||
    return src
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_build_success(app, build, p):
 | 
			
		||||
    build_dir = common.get_build_dir(app)
 | 
			
		||||
 | 
			
		||||
    if os.path.isdir(os.path.join(build_dir, '.git')):
 | 
			
		||||
        import git
 | 
			
		||||
 | 
			
		||||
        commit_id = common.get_head_commit_id(git.repo.Repo(build_dir))
 | 
			
		||||
    else:
 | 
			
		||||
        commit_id = build.commit
 | 
			
		||||
 | 
			
		||||
    if p is not None and p.returncode != 0:
 | 
			
		||||
        raise exception.BuildException(
 | 
			
		||||
            "Build failed for %s:%s@%s" % (app.id, build.versionName, commit_id),
 | 
			
		||||
            p.output,
 | 
			
		||||
        )
 | 
			
		||||
    logging.info(
 | 
			
		||||
        "Successfully built version {versionName} of {appid} from {commit_id}".format(
 | 
			
		||||
            versionName=build.versionName, appid=app.id, commit_id=commit_id
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def execute_postbuild(app, build, src):
 | 
			
		||||
    root_dir = get_build_root_dir(app, build)
 | 
			
		||||
    srclibpaths = get_srclibpaths(app, build)
 | 
			
		||||
 | 
			
		||||
    if build.postbuild:
 | 
			
		||||
        logging.info(f"Running 'postbuild' commands in {root_dir}")
 | 
			
		||||
        cmd = common.replace_config_vars("; ".join(build.postbuild), build)
 | 
			
		||||
 | 
			
		||||
        # Substitute source library paths into commands...
 | 
			
		||||
        for name, number, libpath in srclibpaths:
 | 
			
		||||
            cmd = cmd.replace(f"$${name}$$", str(pathlib.Path.cwd() / libpath))
 | 
			
		||||
 | 
			
		||||
        cmd = cmd.replace('$$OUT$$', str(pathlib.Path(src).resolve()))
 | 
			
		||||
 | 
			
		||||
        p = common.FDroidPopen(
 | 
			
		||||
            ['bash', '-e', '-u', '-o', 'pipefail', '-x', '-c', cmd], cwd=root_dir
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if p.returncode != 0:
 | 
			
		||||
            raise exception.BuildException(
 | 
			
		||||
                "Error running postbuild command for " f"{app.id}:{build.versionName}",
 | 
			
		||||
                p.output,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_metadata_from_apk(app, build, apkfile):
 | 
			
		||||
    """Get the required metadata from the built APK.
 | 
			
		||||
 | 
			
		||||
    VersionName is allowed to be a blank string, i.e. ''
 | 
			
		||||
 | 
			
		||||
    Parameters
 | 
			
		||||
    ----------
 | 
			
		||||
    app
 | 
			
		||||
        The app metadata used to build the APK.
 | 
			
		||||
    build
 | 
			
		||||
        The build that resulted in the APK.
 | 
			
		||||
    apkfile
 | 
			
		||||
        The path of the APK file.
 | 
			
		||||
 | 
			
		||||
    Returns
 | 
			
		||||
    -------
 | 
			
		||||
    versionCode
 | 
			
		||||
        The versionCode from the APK or from the metadata is build.novcheck is
 | 
			
		||||
        set.
 | 
			
		||||
    versionName
 | 
			
		||||
        The versionName from the APK or from the metadata is build.novcheck is
 | 
			
		||||
        set.
 | 
			
		||||
 | 
			
		||||
    Raises
 | 
			
		||||
    ------
 | 
			
		||||
    :exc:`~exception.BuildException`
 | 
			
		||||
        If native code should have been built but was not packaged, no version
 | 
			
		||||
        information or no package ID could be found or there is a mismatch
 | 
			
		||||
        between the package ID in the metadata and the one found in the APK.
 | 
			
		||||
    """
 | 
			
		||||
    appid, versionCode, versionName = common.get_apk_id(apkfile)
 | 
			
		||||
    native_code = common.get_native_code(apkfile)
 | 
			
		||||
 | 
			
		||||
    if build.buildjni and build.buildjni != ['no'] and not native_code:
 | 
			
		||||
        raise exception.BuildException(
 | 
			
		||||
            "Native code should have been built but none was packaged"
 | 
			
		||||
        )
 | 
			
		||||
    if build.novcheck:
 | 
			
		||||
        versionCode = build.versionCode
 | 
			
		||||
        versionName = build.versionName
 | 
			
		||||
    if not versionCode or versionName is None:
 | 
			
		||||
        raise exception.BuildException(
 | 
			
		||||
            "Could not find version information in build in output"
 | 
			
		||||
        )
 | 
			
		||||
    if not appid:
 | 
			
		||||
        raise exception.BuildException("Could not find package ID in output")
 | 
			
		||||
    if appid != app.id:
 | 
			
		||||
        raise exception.BuildException(
 | 
			
		||||
            "Wrong package ID - build " + appid + " but expected " + app.id
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    return versionCode, versionName
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_build_artifacts(app, build, src):
 | 
			
		||||
    # Make sure it's not debuggable...
 | 
			
		||||
    if common.is_debuggable_or_testOnly(src):
 | 
			
		||||
        raise exception.BuildException(
 | 
			
		||||
            "%s: debuggable or testOnly set in AndroidManifest.xml" % src
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # 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 exception.BuildException(
 | 
			
		||||
            "Unsigned APK is not at expected location of " + src
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if common.get_file_extension(src) == 'apk':
 | 
			
		||||
        vercode, version = get_metadata_from_apk(app, build, src)
 | 
			
		||||
        if version != build.versionName or vercode != build.versionCode:
 | 
			
		||||
            raise exception.BuildException(
 | 
			
		||||
                (
 | 
			
		||||
                    "Unexpected version/version code in output;"
 | 
			
		||||
                    " APK: '%s' / '%d', "
 | 
			
		||||
                    " Expected: '%s' / '%d'"
 | 
			
		||||
                )
 | 
			
		||||
                % (version, vercode, build.versionName, build.versionCode)
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def move_build_output(app, build, src, output_dir="unsigned"):
 | 
			
		||||
    # Copy the unsigned APK to our destination directory for further
 | 
			
		||||
    # processing (by publish.py)...
 | 
			
		||||
    dest = os.path.join(
 | 
			
		||||
        output_dir,
 | 
			
		||||
        common.get_release_filename(app, build, common.get_file_extension(src)),
 | 
			
		||||
    )
 | 
			
		||||
    shutil.copyfile(src, dest)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def build_local_run_wrapper(appid, versionCode, config):
 | 
			
		||||
    """Run build for one specific version of an app localy.
 | 
			
		||||
 | 
			
		||||
    :raises: various exceptions in case and of the pre-required conditions for the requested build are not met
 | 
			
		||||
    """
 | 
			
		||||
    app, build = metadata.get_single_build(appid, versionCode)
 | 
			
		||||
 | 
			
		||||
    # not sure if this makes any sense to change open file limits since we know
 | 
			
		||||
    # that this script will only ever build one app
 | 
			
		||||
    rlimit_check()
 | 
			
		||||
 | 
			
		||||
    logging.info(
 | 
			
		||||
        "Building version %s (%s) of %s"
 | 
			
		||||
        % (build.versionName, build.versionCode, app.id)
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # init fdroid Popen wrapper
 | 
			
		||||
    common.set_FDroidPopen_env(app=app, build=build)
 | 
			
		||||
    gradletasks = init_build(app, build, config)
 | 
			
		||||
 | 
			
		||||
    sanitize_build_dir(app)
 | 
			
		||||
 | 
			
		||||
    # this is where we'd call scanner.scan_source() in old build.py
 | 
			
		||||
 | 
			
		||||
    # Run a build command if one is required...
 | 
			
		||||
    execute_build_commands(app, build)
 | 
			
		||||
 | 
			
		||||
    # Build native stuff if required...
 | 
			
		||||
    execute_buildjni_commands(app, build)
 | 
			
		||||
 | 
			
		||||
    # Build the release...
 | 
			
		||||
    p, bindir = execute_build(app, build, config, gradletasks)
 | 
			
		||||
    check_build_success(app, build, p)
 | 
			
		||||
    src = collect_build_output(app, build, p, bindir)
 | 
			
		||||
 | 
			
		||||
    # Run a postbuild command if one is required...
 | 
			
		||||
    execute_postbuild(app, build, src)
 | 
			
		||||
 | 
			
		||||
    validate_build_artifacts(app, build, src)
 | 
			
		||||
 | 
			
		||||
    # this is where we'd call scanner.scan_binary() in old build.py
 | 
			
		||||
 | 
			
		||||
    tmp_dir = pathlib.Path("tmp")
 | 
			
		||||
    tmp_dir.mkdir(exist_ok=True)
 | 
			
		||||
    move_build_output(app, build, src, tmp_dir)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def main():
 | 
			
		||||
    parser = argparse.ArgumentParser(description=__doc__)
 | 
			
		||||
    common.setup_global_opts(parser)
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        "APPID:VERCODE",
 | 
			
		||||
        help="Application ID with Version Code in the form APPID:VERCODE",
 | 
			
		||||
    )
 | 
			
		||||
    options = common.parse_args(parser)
 | 
			
		||||
    config = common.get_config()
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        appid, versionCode = common.split_pkg_arg(options.__dict__['APPID:VERCODE'])
 | 
			
		||||
        build_local_run_wrapper(appid, versionCode, config)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        if options.verbose:
 | 
			
		||||
            traceback.print_exc()
 | 
			
		||||
        else:
 | 
			
		||||
            print(e)
 | 
			
		||||
        sys.exit(1)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    main()
 | 
			
		||||
							
								
								
									
										75
									
								
								fdroidserver/install_ndk.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								fdroidserver/install_ndk.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,75 @@
 | 
			
		|||
#!/usr/bin/env python3
 | 
			
		||||
#
 | 
			
		||||
# install_ndk.py - part of the F-Droid server tools
 | 
			
		||||
# Copyright (C) 2024-2025 Michael Pöhn <michael@poehn.at>
 | 
			
		||||
# Copyright (C) 2025 Hans-Christoph Steiner <hans@eds.org>
 | 
			
		||||
#
 | 
			
		||||
# This program is free software: you can redistribute it and/or modify
 | 
			
		||||
# it under the terms of the GNU Affero General Public License as published by
 | 
			
		||||
# the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
# (at your option) any later version.
 | 
			
		||||
#
 | 
			
		||||
# This program is distributed in the hope that it will be useful,
 | 
			
		||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
# GNU Affero General Public License for more details.
 | 
			
		||||
#
 | 
			
		||||
# You should have received a copy of the GNU Affero General Public License
 | 
			
		||||
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
"""Read the "ndk:" field from the build metadata and install the right NDK packages.
 | 
			
		||||
 | 
			
		||||
For more info, see:
 | 
			
		||||
https://f-droid.org/docs/Build_Metadata_Reference/#build_ndk
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import argparse
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
from fdroidserver import common, exception, metadata
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def install_ndk_wrapper(build, ndk_paths=dict()):
 | 
			
		||||
    """Make sure the requested NDK version is or gets installed.
 | 
			
		||||
 | 
			
		||||
    Parameters
 | 
			
		||||
    ----------
 | 
			
		||||
    build
 | 
			
		||||
        metadata.Build instance entry that may contain the
 | 
			
		||||
        requested NDK version
 | 
			
		||||
    ndk_paths
 | 
			
		||||
        dictionary holding the currently installed NDKs
 | 
			
		||||
    """
 | 
			
		||||
    ndk_path = build.ndk_path()
 | 
			
		||||
    if build.ndk or (build.buildjni and build.buildjni != ['no']):
 | 
			
		||||
        if not ndk_path:
 | 
			
		||||
            for k, v in ndk_paths.items():
 | 
			
		||||
                if k.endswith("_orig"):
 | 
			
		||||
                    continue
 | 
			
		||||
            common.auto_install_ndk(build)
 | 
			
		||||
        ndk_path = build.ndk_path()
 | 
			
		||||
        if not os.path.isdir(ndk_path):
 | 
			
		||||
            logging.critical("Android NDK '%s' is not installed!" % ndk_path)
 | 
			
		||||
            raise exception.FDroidException()
 | 
			
		||||
    return ndk_path
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def main():
 | 
			
		||||
    parser = argparse.ArgumentParser(description=__doc__)
 | 
			
		||||
    common.setup_global_opts(parser)
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        "APPID:VERCODE",
 | 
			
		||||
        help="Application ID with Version Code in the form APPID:VERCODE",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    options = common.parse_args(parser)
 | 
			
		||||
    config = common.get_config()
 | 
			
		||||
    appid, versionCode = common.split_pkg_arg(options.__dict__['APPID:VERCODE'])
 | 
			
		||||
    app, build = metadata.get_single_build(appid, versionCode)
 | 
			
		||||
    install_ndk_wrapper(build, config.get('ndk_paths', dict()))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    main()
 | 
			
		||||
							
								
								
									
										83
									
								
								fdroidserver/make_source_tarball.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								fdroidserver/make_source_tarball.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,83 @@
 | 
			
		|||
#!/usr/bin/env python3
 | 
			
		||||
#
 | 
			
		||||
# make_source_tarball.py - part of the F-Droid server tools
 | 
			
		||||
# Copyright (C) 2024-2025, Michael Pöhn <michael@poehn.at>
 | 
			
		||||
# Copyright (C) 2024-2025, Hans-Christoph Steiner <hans@eds.org>
 | 
			
		||||
# Copyright (C) 2018, Areeb Jamal
 | 
			
		||||
# Copyright (C) 2013-2014, Daniel Martí <mvdan@mvdan.cc>
 | 
			
		||||
# Copyright (C) 2010-2015, Ciaran Gultnieks <ciaran@ciarang.com>
 | 
			
		||||
#
 | 
			
		||||
# This program is free software: you can redistribute it and/or modify
 | 
			
		||||
# it under the terms of the GNU Affero General Public License as published by
 | 
			
		||||
# the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
# (at your option) any later version.
 | 
			
		||||
#
 | 
			
		||||
# This program is distributed in the hope that it will be useful,
 | 
			
		||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
# GNU Affero General Public License for more details.
 | 
			
		||||
#
 | 
			
		||||
# You should have received a copy of the GNU Affero General Public License
 | 
			
		||||
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
"""Make a source tarball from a single app's checked out source code.
 | 
			
		||||
 | 
			
		||||
This assumes that the app's source code is already checked out into the
 | 
			
		||||
repo, and that it is in a clean state.  It makes a src.tar.gz in the
 | 
			
		||||
repo's tmp/ directory.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import argparse
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
import pathlib
 | 
			
		||||
import sys
 | 
			
		||||
import tarfile
 | 
			
		||||
import traceback
 | 
			
		||||
 | 
			
		||||
from fdroidserver import common, metadata
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def make_source_tarball(app, build, output_dir=pathlib.Path('unsigned')):
 | 
			
		||||
    if not output_dir.exists():
 | 
			
		||||
        output_dir.mkdir()
 | 
			
		||||
    build_dir = common.get_build_dir(app)
 | 
			
		||||
    # Build the source tarball right before we build the release...
 | 
			
		||||
    logging.info("Creating source tarball...")
 | 
			
		||||
    tarname = common.get_src_tarball_name(app.id, build.versionCode)
 | 
			
		||||
    tarball = tarfile.open(os.path.join(output_dir, tarname), "w:gz")
 | 
			
		||||
 | 
			
		||||
    def tarexc(t):
 | 
			
		||||
        return (
 | 
			
		||||
            None
 | 
			
		||||
            if any(t.name.endswith(s) for s in ['.svn', '.git', '.hg', '.bzr'])
 | 
			
		||||
            else t
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    tarball.add(build_dir, tarname, filter=tarexc)
 | 
			
		||||
    tarball.close()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def main():
 | 
			
		||||
    parser = argparse.ArgumentParser(description=__doc__)
 | 
			
		||||
    common.setup_global_opts(parser)
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        "APPID:VERCODE",
 | 
			
		||||
        help="Application ID with Version Code in the form APPID:VERCODE",
 | 
			
		||||
    )
 | 
			
		||||
    options = common.parse_args(parser)
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        appid, versionCode = common.split_pkg_arg(options.APPID_VERCODE)
 | 
			
		||||
        make_source_tarball(metadata.get_single_build(appid, versionCode))
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        if options.verbose:
 | 
			
		||||
            traceback.print_exc()
 | 
			
		||||
        else:
 | 
			
		||||
            print(e)
 | 
			
		||||
        sys.exit(1)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    main()
 | 
			
		||||
							
								
								
									
										62
									
								
								fdroidserver/prepare_source.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								fdroidserver/prepare_source.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,62 @@
 | 
			
		|||
#!/usr/bin/env python3
 | 
			
		||||
#
 | 
			
		||||
# prepare_source.py - part of the F-Droid server tools
 | 
			
		||||
# Copyright (C) 2024-2025 Michael Pöhn <michael@poehn.at>
 | 
			
		||||
#
 | 
			
		||||
# This program is free software: you can redistribute it and/or modify
 | 
			
		||||
# it under the terms of the GNU Affero General Public License as published by
 | 
			
		||||
# the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
# (at your option) any later version.
 | 
			
		||||
#
 | 
			
		||||
# This program is distributed in the hope that it will be useful,
 | 
			
		||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
# GNU Affero General Public License for more details.
 | 
			
		||||
#
 | 
			
		||||
# You should have received a copy of the GNU Affero General Public License
 | 
			
		||||
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
"""Prepare the source code directory for a particular build."""
 | 
			
		||||
 | 
			
		||||
import argparse
 | 
			
		||||
import pathlib
 | 
			
		||||
 | 
			
		||||
from fdroidserver import common, metadata
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def main():
 | 
			
		||||
    parser = argparse.ArgumentParser(description=__doc__)
 | 
			
		||||
    common.setup_global_opts(parser)
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        "APPID:VERCODE",
 | 
			
		||||
        help="Application ID with Version Code in the form APPID:VERCODE",
 | 
			
		||||
    )
 | 
			
		||||
    options = common.parse_args(parser)
 | 
			
		||||
    common.get_config()
 | 
			
		||||
    appid, versionCode = common.split_pkg_arg(options.__dict__['APPID:VERCODE'])
 | 
			
		||||
    app, build = metadata.get_single_build(appid, versionCode)
 | 
			
		||||
 | 
			
		||||
    # prepare folders for git/vcs checkout
 | 
			
		||||
    vcs, build_dir = common.setup_vcs(app)
 | 
			
		||||
    srclib_dir = pathlib.Path('./build/srclib')
 | 
			
		||||
    extlib_dir = pathlib.Path('./build/extlib')
 | 
			
		||||
    log_dir = pathlib.Path('./logs')
 | 
			
		||||
    output_dir = pathlib.Path('./unsigned')
 | 
			
		||||
    for d in (srclib_dir, extlib_dir, log_dir, output_dir):
 | 
			
		||||
        d.mkdir(exist_ok=True, parents=True)
 | 
			
		||||
 | 
			
		||||
    # do git/vcs checkout
 | 
			
		||||
    common.prepare_source(
 | 
			
		||||
        vcs,
 | 
			
		||||
        app,
 | 
			
		||||
        build,
 | 
			
		||||
        build_dir,
 | 
			
		||||
        str(srclib_dir),
 | 
			
		||||
        str(extlib_dir),
 | 
			
		||||
        refresh=False,
 | 
			
		||||
        onserver=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    main()
 | 
			
		||||
							
								
								
									
										68
									
								
								tests/test_install_ndk.py
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										68
									
								
								tests/test_install_ndk.py
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,68 @@
 | 
			
		|||
#!/usr/bin/env python3
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
import unittest
 | 
			
		||||
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from unittest import mock
 | 
			
		||||
 | 
			
		||||
from fdroidserver import common, metadata, install_ndk
 | 
			
		||||
from .shared_test_code import mkdtemp, APPID, VERCODE, APPID_VERCODE
 | 
			
		||||
 | 
			
		||||
NDK_RELEASE = 'r24'
 | 
			
		||||
NDK_REVISION = '24.0.8215888'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InstallNdkTest(unittest.TestCase):
 | 
			
		||||
    basedir = Path(__file__).resolve().parent
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self._td = mkdtemp()
 | 
			
		||||
        self.testdir = self._td.name
 | 
			
		||||
        os.chdir(self.testdir)
 | 
			
		||||
        common.config = {'ndk_paths': {}, 'sdk_path': self.testdir}
 | 
			
		||||
 | 
			
		||||
    def tearDown(self):
 | 
			
		||||
        self._td.cleanup()
 | 
			
		||||
        common.config = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def mock_sdkmanager_install(to_install, android_home=None):
 | 
			
		||||
    path = f'{android_home}/{to_install.replace(";", "/")}'
 | 
			
		||||
    ndk_dir = Path(path)
 | 
			
		||||
    ndk_dir.mkdir(parents=True)
 | 
			
		||||
    (ndk_dir / 'source.properties').write_text(f'Pkg.Revision = {NDK_REVISION}\n')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@mock.patch('sdkmanager.build_package_list', lambda use_net: None)
 | 
			
		||||
class InstallNdk_wrapper(InstallNdkTest):
 | 
			
		||||
    @mock.patch('sdkmanager.install')
 | 
			
		||||
    def test_with_ndk(self, sdkmanager_install):
 | 
			
		||||
        sdkmanager_install.side_effect = mock_sdkmanager_install
 | 
			
		||||
        build = metadata.Build({'versionCode': VERCODE, 'ndk': NDK_RELEASE})
 | 
			
		||||
        install_ndk.install_ndk_wrapper(build)
 | 
			
		||||
        sdkmanager_install.assert_called_once()
 | 
			
		||||
 | 
			
		||||
    @mock.patch('fdroidserver.common.auto_install_ndk')
 | 
			
		||||
    def test_without_ndk(self, auto_install_ndk):
 | 
			
		||||
        build = metadata.Build({'versionCode': VERCODE})
 | 
			
		||||
        install_ndk.install_ndk_wrapper(build)
 | 
			
		||||
        auto_install_ndk.assert_not_called()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@mock.patch('sys.argv', ['fdroid ndk', APPID_VERCODE])
 | 
			
		||||
@mock.patch('sdkmanager.build_package_list', lambda use_net: None)
 | 
			
		||||
@mock.patch('sdkmanager.install')
 | 
			
		||||
class InstallNdk_main(InstallNdkTest):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        super().setUp()
 | 
			
		||||
        metadatapath = Path(common.get_metadatapath(APPID))
 | 
			
		||||
        metadatapath.parent.mkdir()
 | 
			
		||||
        metadatapath.write_text(
 | 
			
		||||
            f'Builds:\n - versionCode: {VERCODE}\n   ndk: {NDK_RELEASE}\n'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_ndk_main(self, sdkmanager_install):
 | 
			
		||||
        sdkmanager_install.side_effect = mock_sdkmanager_install
 | 
			
		||||
        install_ndk.main()
 | 
			
		||||
        sdkmanager_install.assert_called_once()
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue