• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kivy / python-for-android / 26681571906

30 May 2026 10:31AM UTC coverage: 62.67% (-1.2%) from 63.887%
26681571906

Pull #3278

github

web-flow
Merge 117fe4eef into 74b559a3c
Pull Request #3278: Handling system bars and Edge-to-Edge enforcement (android 15+)

1832 of 3194 branches covered (57.36%)

Branch coverage included in aggregate %.

5407 of 8357 relevant lines covered (64.7%)

3.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.85
/pythonforandroid/recipes/hostpython3/__init__.py
1
import sh
6✔
2
import os
6✔
3

4
from multiprocessing import cpu_count
6✔
5
from pathlib import Path
6✔
6
from os.path import join
6✔
7

8
from packaging.version import Version
6✔
9
from pythonforandroid.logger import shprint
6✔
10
from pythonforandroid.recipe import Recipe
6✔
11
from pythonforandroid.util import (
6✔
12
    BuildInterruptingException,
13
    current_directory,
14
    ensure_dir,
15
)
16
from pythonforandroid.prerequisites import OpenSSLPrerequisite
6✔
17

18
HOSTPYTHON_VERSION_UNSET_MESSAGE = "The hostpython recipe must have set version"
6✔
19

20
SETUP_DIST_NOT_FIND_MESSAGE = "Could not find Setup.dist or Setup in Python build"
6✔
21

22

23
class HostPython3Recipe(Recipe):
6✔
24
    """
25
    The hostpython3's recipe.
26

27
    .. versionchanged:: 2019.10.06.post0
28
        Refactored from deleted class ``python.HostPythonRecipe`` into here.
29

30
    .. versionchanged:: 0.6.0
31
        Refactored into  the new class
32
        :class:`~pythonforandroid.python.HostPythonRecipe`
33
    """
34

35
    version = "3.14.2"
6✔
36

37
    url = "https://github.com/python/cpython/archive/refs/tags/v{version}.tar.gz"
6✔
38
    """The default url to download our host python recipe. This url will
6✔
39
    change depending on the python version set in attribute :attr:`version`."""
40

41
    build_subdir = "native-build"
6✔
42
    """Specify the sub build directory for the hostpython3 recipe. Defaults
6✔
43
    to ``native-build``."""
44

45
    patches = ["fix_ensurepip.patch"]
6✔
46

47
    # apply version guard
48
    def download(self):
6✔
49
        python_recipe = Recipe.get_recipe("python3", self.ctx)
×
50
        if python_recipe.version != self.version:
×
51
            raise BuildInterruptingException(
×
52
                f"python3 should have same version as hostpython3, {python_recipe.version} != {self.version}"
53
            )
54
        super().download()
×
55

56
    @property
6✔
57
    def _exe_name(self):
6✔
58
        """
59
        Returns the name of the python executable depending on the version.
60
        """
61
        if not self.version:
6✔
62
            raise BuildInterruptingException(HOSTPYTHON_VERSION_UNSET_MESSAGE)
6✔
63
        return "python"
6✔
64

65
    @property
6✔
66
    def python_exe(self):
6✔
67
        """Returns the full path of the hostpython executable."""
68
        return join(self.local_bin, self._exe_name)
6✔
69

70
    def get_recipe_env(self, arch=None):
6✔
71
        env = os.environ.copy()
6✔
72
        openssl_prereq = OpenSSLPrerequisite()
6✔
73
        if env.get("PKG_CONFIG_PATH", ""):
6!
74
            env["PKG_CONFIG_PATH"] = os.pathsep.join(
6✔
75
                [openssl_prereq.pkg_config_location, env["PKG_CONFIG_PATH"]]
76
            )
77
        else:
78
            env["PKG_CONFIG_PATH"] = openssl_prereq.pkg_config_location
×
79
        return env
6✔
80

81
    def should_build(self, arch):
6✔
82
        if Path(self.python_exe).exists():
6✔
83
            # no need to build, but we must set hostpython for our Context
84
            self.ctx.hostpython = self.python_exe
6✔
85
            return False
6✔
86
        return True
6✔
87

88
    def get_build_container_dir(self, arch=None):
6✔
89
        choices = self.check_recipe_choices()
6✔
90
        dir_name = "-".join([self.name] + choices)
6✔
91
        return join(self.ctx.build_dir, "other_builds", dir_name, "desktop")
6✔
92

93
    def get_build_dir(self, arch=None):
6✔
94
        """
95
        .. note:: Unlike other recipes, the hostpython build dir doesn't
96
            depend on the target arch
97
        """
98
        return join(self.get_build_container_dir(), self.name)
6✔
99

100
    def get_path_to_python(self):
6✔
101
        return join(self.get_build_dir(), self.build_subdir)
6✔
102

103
    @property
6✔
104
    def site_root(self):
6✔
105
        return join(self.get_path_to_python(), "root")
6✔
106

107
    @property
6✔
108
    def site_bin(self):
6✔
109
        return join(self.site_root, self.site_dir, "bin")
6✔
110

111
    @property
6✔
112
    def local_dir(self):
6✔
113
        return join(self.site_root, "usr/local/")
6✔
114

115
    @property
6✔
116
    def local_bin(self):
6✔
117
        return join(self.local_dir, "bin")
6✔
118

119
    @property
6✔
120
    def site_dir(self):
6✔
121
        p_version = Version(self.version)
6✔
122
        return join(
6✔
123
            self.site_root,
124
            f"usr/local/lib/python{p_version.major}.{p_version.minor}/site-packages/",
125
        )
126

127
    @property
6✔
128
    def _pip(self):
6✔
129
        return join(self.local_bin, "pip3")
×
130

131
    @property
6✔
132
    def pip(self):
6✔
133
        return sh.Command(self._pip)
×
134

135
    def fix_pip_shebangs(self):
6✔
136

137
        if not os.path.exists(self.local_bin):
6!
138
            return
6✔
139

140
        for filename in os.listdir(self.local_bin):
×
141
            if not filename.startswith("pip"):
×
142
                continue
×
143

144
            pip_path = os.path.join(self.local_bin, filename)
×
145

146
            with open(pip_path, "rb") as file:
×
147
                file_lines = file.read().splitlines()
×
148

149
            file_lines[0] = f"#!{self.python_exe}".encode()
×
150

151
            with open(pip_path, "wb") as file:
×
152
                file.write(b"\n".join(file_lines) + b"\n")
×
153

154
    def build_arch(self, arch):
6✔
155
        env = self.get_recipe_env(arch)
6✔
156

157
        recipe_build_dir = self.get_build_dir(arch.arch)
6✔
158

159
        # Create a subdirectory to actually perform the build
160
        build_dir = join(recipe_build_dir, self.build_subdir)
6✔
161
        ensure_dir(build_dir)
6✔
162

163
        # Configure the build
164
        build_configured = False
6✔
165
        with current_directory(build_dir):
6✔
166
            if not Path("config.status").exists():
6✔
167
                shprint(
6✔
168
                    sh.Command(join(recipe_build_dir, "configure")),
169
                    "--prefix",
170
                    self.local_dir,
171
                    _env=env,
172
                )
173
                build_configured = True
6✔
174

175
        with current_directory(recipe_build_dir):
6✔
176
            # Create the Setup file. This copying from Setup.dist is
177
            # the normal and expected procedure before Python 3.8, but
178
            # after this the file with default options is already named "Setup"
179
            setup_dist_location = join("Modules", "Setup.dist")
6✔
180
            if Path(setup_dist_location).exists():
6✔
181
                shprint(sh.cp, setup_dist_location, join(build_dir, "Modules", "Setup"))
6✔
182
            else:
183
                # Check the expected file does exist
184
                setup_location = join("Modules", "Setup")
6✔
185
                if not Path(setup_location).exists():
6✔
186
                    raise BuildInterruptingException(SETUP_DIST_NOT_FIND_MESSAGE)
6✔
187

188
            shprint(sh.make, "-j", str(cpu_count()), "-C", build_dir, _env=env)
6✔
189

190
        with current_directory(build_dir):
6✔
191
            shprint(sh.make, "install", _env=env)
6✔
192

193
        with current_directory(recipe_build_dir):
6✔
194
            # make a copy of the python executable giving it the name we want,
195
            # because we got different python's executable names depending on
196
            # the fs being case-insensitive (Mac OS X, Cygwin...) or
197
            # case-sensitive (linux)...so this way we will have an unique name
198
            # for our hostpython, regarding the used fs
199
            for exe_name in ["python.exe", "python"]:
6!
200
                exe = join(self.get_path_to_python(), exe_name)
6✔
201
                if Path(exe).is_file():
6!
202
                    shprint(sh.cp, exe, self.python_exe)
6✔
203
                    break
6✔
204

205
        ensure_dir(self.site_root)
6✔
206
        self.ctx.hostpython = self.python_exe
6✔
207

208
        if build_configured:
6✔
209
            shprint(
6✔
210
                sh.Command(self.python_exe),
211
                "-m",
212
                "ensurepip",
213
                "-U",
214
                _env={"HOME": "/tmp", "PATH": self.local_bin},
215
            )
216
            self.fix_pip_shebangs()
6✔
217

218

219
recipe = HostPython3Recipe()
6✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc