Skip to content

Commit e8e3150

Browse files
committed
ENH: Add support for cross-compiling Android with cibuildwheel
1 parent 3205db1 commit e8e3150

2 files changed

Lines changed: 93 additions & 0 deletions

File tree

mesonpy/__init__.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,52 @@ def __init__(
744744
self._meson_cross_file.write_text(cross_file_data, encoding='utf-8')
745745
self._meson_args['setup'].extend(('--cross-file', os.fspath(self._meson_cross_file)))
746746

747+
# Simplify cross-compilation for Android with cibuildwheel: detect the
748+
# cross-compilation environment set up by cibuildwheel and synthesize an
749+
# appropriate cross file.
750+
elif sysconfig.get_platform().startswith('android-') and 'CIBUILDWHEEL' in os.environ:
751+
cpu = platform.machine().lower()
752+
753+
# Python is inconsistent in its platform module, e.g. for x86 it might
754+
# return 'x86', 'i686' or some such. Convert to a canonical name recognized
755+
# by Meson (https://mesonbuild.com/Reference-tables.html#cpu-families).
756+
# Based on mesonbuild.envconfig.detect_cpu_family, but only including
757+
# architectures used by Android.
758+
if cpu.startswith('i') and cpu.endswith('86'):
759+
cpu_family = 'x86'
760+
elif cpu == 'arm64':
761+
cpu_family = 'aarch64'
762+
elif cpu.startswith('aarch64'):
763+
# This can be `aarch64_be`
764+
cpu_family = 'aarch64'
765+
elif cpu.startswith('arm') or cpu.startswith('earm'):
766+
cpu_family = 'arm'
767+
elif cpu in {'amd64', 'x64', 'i86pc'}:
768+
cpu_family = 'x86_64'
769+
else:
770+
cpu_family = cpu
771+
772+
cross_file_data = textwrap.dedent(f'''
773+
[host_machine]
774+
system = 'android'
775+
subsystem = 'android'
776+
kernel = 'linux'
777+
cpu_family = {cpu_family!r}
778+
cpu = {cpu!r}
779+
endian = {sys.byteorder!r}
780+
781+
[properties]
782+
# Due to Python lacking proper cross-compilation support, for the build
783+
# to produce the correct wheel tags when cross-compiling for Android,
784+
# cibuildwheel monkey-patches platform.system() and platform.machine()
785+
# to simulate running on Android. This makes Meson believe that host and
786+
# build machines match and thus that host binaries can be run on the build
787+
# machine, when this is not actually the case. Override the auto-detection.
788+
needs_exe_wrapper = true
789+
''')
790+
self._meson_cross_file.write_text(cross_file_data, encoding='utf-8')
791+
self._meson_args['setup'].extend(('--cross-file', os.fspath(self._meson_cross_file)))
792+
747793
# Support iOS targets. iOS does not have native build tools and always
748794
# requires cross compilation: synthesize the appropriate cross file.
749795
elif sysconfig.get_platform().startswith('ios-'):

tests/test_project.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,53 @@ def test_archflags_envvar_parsing_invalid(package_purelib_and_platlib, monkeypat
378378
os.environ.pop('_PYTHON_HOST_PLATFORM', None)
379379

380380

381+
@pytest.mark.parametrize(
382+
('cpu', 'cpu_family'),
383+
[
384+
('aarch64', 'aarch64'),
385+
('armv7l', 'arm'),
386+
('armv8l', 'arm'),
387+
('i686', 'x86'),
388+
('x86_64', 'x86_64'),
389+
],
390+
)
391+
@pytest.mark.parametrize('cross', [True, False])
392+
def test_android_project(package_simple, monkeypatch, tmp_path, cpu, cpu_family, cross):
393+
# Mock being on Android
394+
monkeypatch.setattr(sys, 'platform', 'android')
395+
monkeypatch.setattr(sys, 'byteorder', 'little')
396+
monkeypatch.setattr(platform, 'system', Mock(return_value='Android'))
397+
monkeypatch.setattr(platform, 'machine', Mock(return_value=cpu))
398+
monkeypatch.setattr(sysconfig, 'get_platform', Mock(return_value='android-24'))
399+
if cross:
400+
monkeypatch.setenv('CIBUILDWHEEL', '1')
401+
402+
# Meson may require some tools to be configured when fatal warnings are enabled.
403+
# Set the same set of variables as cibuildwheel.
404+
for name in ['ar', 'as', 'cc', 'cxx', 'ld', 'nm', 'ranlib', 'readelf', 'strip']:
405+
monkeypatch.setenv(name.upper(), f'/path/to/{name}')
406+
407+
# Create a project.
408+
project = mesonpy.Project(source_dir=package_simple, build_dir=tmp_path)
409+
410+
# When cross-compiling, a cross file should be generated and used.
411+
setup_args = project._meson_args['setup']
412+
cross_path = tmp_path / 'meson-python-cross-file.ini'
413+
if cross:
414+
assert setup_args[-2:] == ['--cross-file', str(cross_path)]
415+
cross_config = cross_path.read_text().splitlines()
416+
assert "system = 'android'" in cross_config
417+
assert "subsystem = 'android'" in cross_config
418+
assert "kernel = 'linux'" in cross_config
419+
assert f"cpu_family = '{cpu_family}'" in cross_config
420+
assert f"cpu = '{cpu}'" in cross_config
421+
assert "endian = 'little'" in cross_config
422+
assert 'needs_exe_wrapper = true' in cross_config
423+
else:
424+
assert '--cross-file' not in setup_args
425+
assert not cross_path.exists()
426+
427+
381428
@pytest.mark.skipif(sys.version_info < (3, 13), reason='requires Python 3.13 or higher')
382429
@pytest.mark.parametrize('multiarch', [
383430
'arm64-iphoneos',

0 commit comments

Comments
 (0)