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

kivy / python-for-android / 24545265390

17 Apr 2026 02:53AM UTC coverage: 63.447% (+0.07%) from 63.382%
24545265390

Pull #3301

github

T-Dynamos
fix current dir
Pull Request #3301: fix PYTHONPATH hacks

1830 of 3152 branches covered (58.06%)

Branch coverage included in aggregate %.

51 of 63 new or added lines in 5 files covered. (80.95%)

3 existing lines in 1 file now uncovered.

5323 of 8122 relevant lines covered (65.54%)

5.23 hits per line

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

84.92
/pythonforandroid/recipes/hostpython3/__init__.py
1
import sh
8 ✔
2
import os
8 ✔
3

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

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

18
HOSTPYTHON_VERSION_UNSET_MESSAGE = (
8 ✔
19
    'The hostpython recipe must have set version'
20
)
21

22
SETUP_DIST_NOT_FIND_MESSAGE = (
8 ✔
23
    'Could not find Setup.dist or Setup in Python build'
24
)
25

26

27
class HostPython3Recipe(Recipe):
8 ✔
28
    '''
29
    The hostpython3's recipe.
30

31
    .. versionchanged:: 2019.10.06.post0
32
        Refactored from deleted class ``python.HostPythonRecipe`` into here.
33

34
    .. versionchanged:: 0.6.0
35
        Refactored into  the new class
36
        :class:`~pythonforandroid.python.HostPythonRecipe`
37
    '''
38

39
    version = '3.14.2'
8 ✔
40

41
    url = 'https://github.com/python/cpython/archive/refs/tags/v{version}.tar.gz'
8 ✔
42
    '''The default url to download our host python recipe. This url will
6 ✔
43
    change depending on the python version set in attribute :attr:`version`.'''
44

45
    build_subdir = 'native-build'
8 ✔
46
    '''Specify the sub build directory for the hostpython3 recipe. Defaults
6 ✔
47
    to ``native-build``.'''
48

49
    patches = ["fix_ensurepip.patch"]
8 ✔
50

51
    # apply version guard
52
    def download(self):
8 ✔
53
        python_recipe = Recipe.get_recipe("python3", self.ctx)
×
54
        if python_recipe.version != self.version:
×
55
            raise BuildInterruptingException(
×
56
                f"python3 should have same version as hostpython3, {python_recipe.version} != {self.version}"
57
            )
NEW
58
        super().download()
×
59

60
    @property
8 ✔
61
    def _exe_name(self):
8 ✔
62
        '''
63
        Returns the name of the python executable depending on the version.
64
        '''
65
        if not self.version:
8 ✔
66
            raise BuildInterruptingException(HOSTPYTHON_VERSION_UNSET_MESSAGE)
8 ✔
67
        return f'python{self.version.split(".")[0]}'
8 ✔
68

69
    @property
8 ✔
70
    def python_exe(self):
8 ✔
71
        '''Returns the full path of the hostpython executable.'''
72
        return join(self.local_bin, self._exe_name)
8 ✔
73

74
    def get_recipe_env(self, arch=None):
8 ✔
75
        env = os.environ.copy()
8 ✔
76
        openssl_prereq = OpenSSLPrerequisite()
8 ✔
77
        if env.get("PKG_CONFIG_PATH", ""):
8 !
78
            env["PKG_CONFIG_PATH"] = os.pathsep.join(
8 ✔
79
                [openssl_prereq.pkg_config_location, env["PKG_CONFIG_PATH"]]
80
            )
81
        else:
82
            env["PKG_CONFIG_PATH"] = openssl_prereq.pkg_config_location
×
83
        return env
8 ✔
84

85
    def should_build(self, arch):
8 ✔
86
        if Path(self.python_exe).exists():
8 ✔
87
            # no need to build, but we must set hostpython for our Context
88
            self.ctx.hostpython = self.python_exe
8 ✔
89
            return False
8 ✔
90
        return True
8 ✔
91

92
    def get_build_container_dir(self, arch=None):
8 ✔
93
        choices = self.check_recipe_choices()
8 ✔
94
        dir_name = '-'.join([self.name] + choices)
8 ✔
95
        return join(self.ctx.build_dir, 'other_builds', dir_name, 'desktop')
8 ✔
96

97
    def get_build_dir(self, arch=None):
8 ✔
98
        '''
99
        .. note:: Unlike other recipes, the hostpython build dir doesn't
100
            depend on the target arch
101
        '''
102
        return join(self.get_build_container_dir(), self.name)
8 ✔
103

104
    def get_path_to_python(self):
8 ✔
105
        return join(self.get_build_dir(), self.build_subdir)
8 ✔
106

107
    @property
8 ✔
108
    def site_root(self):
8 ✔
109
        return join(self.get_path_to_python(), "root")
8 ✔
110

111
    @property
8 ✔
112
    def site_bin(self):
8 ✔
113
        return join(self.site_root, self.site_dir, "bin")
8 ✔
114

115
    @property
8 ✔
116
    def local_dir(self):
8 ✔
117
        return join(self.site_root, "usr/local/")
8 ✔
118

119
    @property
8 ✔
120
    def local_bin(self):
8 ✔
121
        return join(self.local_dir, "bin")
8 ✔
122

123
    @property
8 ✔
124
    def site_dir(self):
8 ✔
125
        p_version = Version(self.version)
8 ✔
126
        return join(
8 ✔
127
            self.site_root,
128
            f"usr/local/lib/python{p_version.major}.{p_version.minor}/site-packages/"
129
        )
130

131
    @property
8 ✔
132
    def _pip(self):
8 ✔
133
        return join(self.local_bin, "pip3")
×
134

135
    @property
8 ✔
136
    def pip(self):
8 ✔
137
        return sh.Command(self._pip)
×
138

139
    def fix_pip_shebangs(self):
8 ✔
140

141
        if not os.path.exists(self.local_bin):
8 !
142
            return
8 ✔
143

144
        for filename in os.listdir(self.local_bin):
×
145
            if not filename.startswith("pip"):
×
146
                continue
×
147

148
            pip_path = os.path.join(self.local_bin, filename)
×
149

150
            with open(pip_path, "rb") as file:
×
151
                file_lines = file.read().splitlines()
×
152

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

155
            with open(pip_path, "wb") as file:
×
156
                file.write(b"\n".join(file_lines) + b"\n")
×
157

158
    def build_arch(self, arch):
8 ✔
159
        env = self.get_recipe_env(arch)
8 ✔
160

161
        recipe_build_dir = self.get_build_dir(arch.arch)
8 ✔
162

163
        # Create a subdirectory to actually perform the build
164
        build_dir = join(recipe_build_dir, self.build_subdir)
8 ✔
165
        ensure_dir(build_dir)
8 ✔
166

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

175
        with current_directory(recipe_build_dir):
8 ✔
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')
8 ✔
180
            if Path(setup_dist_location).exists():
8 ✔
181
                shprint(sh.cp, setup_dist_location,
8 ✔
182
                        join(build_dir, 'Modules', 'Setup'))
183
            else:
184
                # Check the expected file does exist
185
                setup_location = join('Modules', 'Setup')
8 ✔
186
                if not Path(setup_location).exists():
8 ✔
187
                    raise BuildInterruptingException(
8 ✔
188
                        SETUP_DIST_NOT_FIND_MESSAGE
189
                    )
190

191
            shprint(sh.make, '-j', str(cpu_count()), '-C', build_dir, _env=env)
8 ✔
192

193
        with current_directory(build_dir):
8 ✔
194
            shprint(sh.make, 'install', _env=env)
8 ✔
195

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

208
        ensure_dir(self.site_root)
8 ✔
209
        self.ctx.hostpython = self.python_exe
8 ✔
210

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

218

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

© 2026 Coveralls, Inc