Skip to content

Commit afe50a8

Browse files
committed
🐛 Resolve Meson tools through recipe env
MesonRecipe installs meson and ninja as hostpython prerequisites and PyProjectRecipe exposes those executables by prepending the hostpython bin directories to the recipe environment PATH. Recipes that call sh.meson or sh.ninja directly can fail before _env is applied when those tools are not also visible on the current Python process PATH. Add shared MesonRecipe helpers that resolve commands against the recipe env PATH and return explicit sh.Command instances. Use those helpers in the libthorvg and libcairo manual Meson/Ninja build steps. Add a regression test that reproduces the environment-sensitive lookup failure and verifies the helper resolves meson from env PATH.
1 parent a73e508 commit afe50a8

4 files changed

Lines changed: 65 additions & 7 deletions

File tree

pythonforandroid/recipe.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1567,6 +1567,18 @@ def ensure_args(self, *args):
15671567
if arg not in self.extra_build_args:
15681568
self.extra_build_args.append(arg)
15691569

1570+
def get_recipe_env_command(self, command, env):
1571+
command_path = shutil.which(command, path=env["PATH"])
1572+
if command_path is None:
1573+
raise sh.CommandNotFound(command)
1574+
return sh.Command(command_path)
1575+
1576+
def get_meson_command(self, env):
1577+
return self.get_recipe_env_command("meson", env)
1578+
1579+
def get_ninja_command(self, env):
1580+
return self.get_recipe_env_command("ninja", env)
1581+
15701582
def build_arch(self, arch):
15711583
cross_file = join("/tmp", "android.meson.cross")
15721584
info("Writing cross file at: {}".format(cross_file))

pythonforandroid/recipes/libcairo/__init__.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def build_arch(self, arch):
5656
_env=env
5757
)
5858

59-
shprint(sh.meson, 'setup', 'builddir',
59+
shprint(self.get_meson_command(env), 'setup', 'builddir',
6060
'--cross-file', join("/tmp", "android.meson.cross"),
6161
f'--prefix={install_dir}',
6262
'-Dpng=enabled',
@@ -72,14 +72,22 @@ def build_arch(self, arch):
7272
f'-Dfreetype_lib_dir={lib_dir}',
7373
_env=env)
7474

75-
shprint(sh.ninja, '-C', 'builddir', '-j', str(cpu_count()), _env=env)
75+
shprint(
76+
self.get_ninja_command(env),
77+
'-C', 'builddir', '-j', str(cpu_count()),
78+
_env=env
79+
)
7680
# macOS fix: sometimes Ninja creates a dummy 'lib' file instead of a directory.
7781
# So we remove and recreate the install directory using shell commands,
7882
# since os.remove/os.makedirs behave inconsistently in this build env.
7983
shprint(sh.rm, '-rf', install_dir)
8084
shprint(sh.mkdir, install_dir)
8185

82-
shprint(sh.ninja, '-C', 'builddir', 'install', _env=env)
86+
shprint(
87+
self.get_ninja_command(env),
88+
'-C', 'builddir', 'install',
89+
_env=env
90+
)
8391

8492

8593
recipe = LibCairoRecipe()

pythonforandroid/recipes/libthorvg/__init__.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def build_arch(self, arch):
5454
with current_directory(build_dir):
5555

5656
shprint(
57-
sh.meson,
57+
self.get_meson_command(env),
5858
"setup",
5959
"builddir",
6060
"--cross-file",
@@ -72,10 +72,18 @@ def build_arch(self, arch):
7272
_env=env,
7373
)
7474

75-
shprint(sh.ninja, "-C", "builddir", "-j", str(cpu_count()), _env=env)
75+
shprint(
76+
self.get_ninja_command(env),
77+
"-C", "builddir", "-j", str(cpu_count()),
78+
_env=env,
79+
)
7680
shprint(sh.rm, "-rf", install_dir)
7781
shprint(sh.mkdir, install_dir)
78-
shprint(sh.ninja, "-C", "builddir", "install", _env=env)
82+
shprint(
83+
self.get_ninja_command(env),
84+
"-C", "builddir", "install",
85+
_env=env,
86+
)
7987

8088
# copy libomp.so
8189
arch_map = {

tests/test_recipe.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import os
22
import pytest
3+
import sh
34
import tempfile
45
import types
56
import unittest
67
import warnings
78
from unittest import mock
89

910
from pythonforandroid.build import Context
10-
from pythonforandroid.recipe import Recipe, TargetPythonRecipe, import_recipe
11+
from pythonforandroid.recipe import (
12+
MesonRecipe, Recipe, TargetPythonRecipe, import_recipe
13+
)
1114
from pythonforandroid.archs import ArchAarch_64
1215
from pythonforandroid.bootstrap import Bootstrap
1316
from tests.test_bootstrap import BaseClassSetupBootstrap
@@ -198,6 +201,33 @@ class DummyTargetPythonRecipe(TargetPythonRecipe):
198201
assert recipe.major_minor_version_string == '1.2'
199202

200203

204+
class TestMesonRecipe(unittest.TestCase):
205+
206+
def test_get_recipe_env_command_uses_env_path(self):
207+
"""
208+
Meson commands can be installed in the hostpython environment without
209+
being visible on the current Python process PATH.
210+
"""
211+
with tempfile.TemporaryDirectory() as temp_dir:
212+
bin_dir = os.path.join(temp_dir, "bin")
213+
os.mkdir(bin_dir)
214+
meson_path = os.path.join(bin_dir, "meson")
215+
with open(meson_path, "w") as file:
216+
file.write("#!/bin/sh\necho fake meson\n")
217+
os.chmod(meson_path, 0o755)
218+
219+
env = {"PATH": bin_dir}
220+
recipe = MesonRecipe()
221+
222+
with mock.patch.dict(os.environ, {"PATH": os.devnull}):
223+
with pytest.raises(sh.CommandNotFound):
224+
sh.meson("--version", _env=env)
225+
226+
meson = recipe.get_meson_command(env)
227+
228+
assert meson("--version").strip() == "fake meson"
229+
230+
201231
class TestLibraryRecipe(BaseClassSetupBootstrap, unittest.TestCase):
202232
def setUp(self):
203233
"""

0 commit comments

Comments
 (0)