diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 36e1f17d..1f44e957 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -744,6 +744,35 @@ def __init__( self._meson_cross_file.write_text(cross_file_data, encoding='utf-8') self._meson_args['setup'].extend(('--cross-file', os.fspath(self._meson_cross_file))) + # Android may be native-compiled (e.g. with Termux), cross-compiled with manual + # Meson arguments, or cross-compiled with cibuildwheel, which uses a similar + # approach to crossenv. In the latter case, we synthesize a cross file to make + # that work out of the box. + elif sysconfig.get_platform().startswith('android-') and 'CIBUILDWHEEL' in os.environ: + cpu = platform.machine() + cpu_family = 'arm' if cpu.startswith('arm') else 'x86' if cpu.endswith('86') else cpu + + cross_file_data = textwrap.dedent(f''' + [host_machine] + system = 'android' + subsystem = 'android' + kernel = 'linux' + cpu_family = {cpu_family!r} + cpu = {cpu!r} + endian = {sys.byteorder!r} + + [properties] + # cibuildwheel monkey-patches platform.system and platform.machine to + # simulate running on Android, when it's actually running on Linux or + # macOS. So the host and build machines will appear to match, but host + # binaries cannot actually be run on the build machine. As described at + # https://mesonbuild.com/Cross-compilation.html, we indicate this by + # setting needs_exe_wrapper = true, but NOT setting exe_wrapper. + needs_exe_wrapper = true + ''') + self._meson_cross_file.write_text(cross_file_data, encoding='utf-8') + self._meson_args['setup'].extend(('--cross-file', os.fspath(self._meson_cross_file))) + # Support iOS targets. iOS does not have native build tools and always # requires cross compilation: synthesize the appropriate cross file. elif sysconfig.get_platform().startswith('ios-'): diff --git a/tests/test_project.py b/tests/test_project.py index b511b217..76267964 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -378,6 +378,53 @@ def test_archflags_envvar_parsing_invalid(package_purelib_and_platlib, monkeypat os.environ.pop('_PYTHON_HOST_PLATFORM', None) +@pytest.mark.parametrize( + ('cpu', 'cpu_family'), + [ + ('aarch64', 'aarch64'), + ('armv7l', 'arm'), + ('armv8l', 'arm'), + ('i686', 'x86'), + ('x86_64', 'x86_64'), + ], +) +@pytest.mark.parametrize('cross', [True, False]) +def test_android_project(package_simple, monkeypatch, tmp_path, cpu, cpu_family, cross): + # Mock being on Android + monkeypatch.setattr(sys, 'platform', 'android') + monkeypatch.setattr(sys, 'byteorder', 'little') + monkeypatch.setattr(platform, 'system', Mock(return_value='Android')) + monkeypatch.setattr(platform, 'machine', Mock(return_value=cpu)) + monkeypatch.setattr(sysconfig, 'get_platform', Mock(return_value='android-24')) + if cross: + monkeypatch.setenv('CIBUILDWHEEL', '1') + + # Meson may require some tools to be configured when fatal warnings are enabled. + # Set the same set of variables as cibuildwheel. + for name in ['ar', 'as', 'cc', 'cxx', 'ld', 'nm', 'ranlib', 'readelf', 'strip']: + monkeypatch.setenv(name.upper(), f'/path/to/{name}') + + # Create a project. + project = mesonpy.Project(source_dir=package_simple, build_dir=tmp_path) + + # When cross-compiling, a cross file should be generated and used. + setup_args = project._meson_args['setup'] + cross_path = tmp_path / 'meson-python-cross-file.ini' + if cross: + assert setup_args[-2:] == ['--cross-file', str(cross_path)] + cross_config = cross_path.read_text().splitlines() + assert "system = 'android'" in cross_config + assert "subsystem = 'android'" in cross_config + assert "kernel = 'linux'" in cross_config + assert f"cpu_family = '{cpu_family}'" in cross_config + assert f"cpu = '{cpu}'" in cross_config + assert "endian = 'little'" in cross_config + assert 'needs_exe_wrapper = true' in cross_config + else: + assert '--cross-file' not in setup_args + assert not cross_path.exists() + + @pytest.mark.skipif(sys.version_info < (3, 13), reason='requires Python 3.13 or higher') @pytest.mark.parametrize('multiarch', [ 'arm64-iphoneos',