1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375
|
# SPDX-FileCopyrightText: 2021 The meson-python developers
#
# SPDX-License-Identifier: MIT
import ast
import os
import shutil
import sys
import textwrap
if sys.version_info < (3, 11):
import tomli as tomllib
else:
import tomllib
import pyproject_metadata
import pytest
import mesonpy
from .conftest import MESON_VERSION, in_git_repo_context, metadata, package_dir
def test_unsupported_python_version(package_unsupported_python_version):
with pytest.raises(mesonpy.MesonBuilderError, match='The package requires Python version ==1.0.0'):
with mesonpy._project():
pass
def test_missing_meson_version(package_missing_meson_version):
with pytest.raises(pyproject_metadata.ConfigurationError, match='Section "project" missing in pyproject.toml'):
with mesonpy._project():
pass
def test_missing_dynamic_version(package_missing_dynamic_version):
with pytest.raises(pyproject_metadata.ConfigurationError, match='Field "version" declared as dynamic but'):
with mesonpy._project():
pass
@pytest.mark.skipif(MESON_VERSION < (1, 6, 0), reason='meson too old')
@pytest.mark.filterwarnings('ignore:canonicalization and validation of license expression')
def test_meson_build_metadata(tmp_path):
tmp_path.joinpath('pyproject.toml').write_text(textwrap.dedent('''
[build-system]
build-backend = 'mesonpy'
requires = ['meson-python']
'''), encoding='utf8')
tmp_path.joinpath('meson.build').write_text(textwrap.dedent('''
project('test', version: '1.2.3', license: 'MIT', license_files: 'LICENSE')
'''), encoding='utf8')
tmp_path.joinpath('LICENSE').write_text('')
p = mesonpy.Project(tmp_path, tmp_path / 'build')
assert metadata(bytes(p._metadata.as_rfc822())) == metadata(textwrap.dedent('''\
Metadata-Version: 2.4
Name: test
Version: 1.2.3
License-Expression: MIT
License-File: LICENSE
'''))
@pytest.mark.skipif(MESON_VERSION < (1, 6, 0), reason='meson too old')
@pytest.mark.filterwarnings('ignore:canonicalization and validation of license expression')
def test_dynamic_license(tmp_path):
tmp_path.joinpath('pyproject.toml').write_text(textwrap.dedent('''
[build-system]
build-backend = 'mesonpy'
requires = ['meson-python']
[project]
name = 'test'
version = '1.0.0'
dynamic = ['license']
'''), encoding='utf8')
tmp_path.joinpath('meson.build').write_text(textwrap.dedent('''
project('test', license: 'MIT')
'''), encoding='utf8')
p = mesonpy.Project(tmp_path, tmp_path / 'build')
assert metadata(bytes(p._metadata.as_rfc822())) == metadata(textwrap.dedent('''\
Metadata-Version: 2.4
Name: test
Version: 1.0.0
License-Expression: MIT
'''))
@pytest.mark.skipif(MESON_VERSION < (1, 6, 0), reason='meson too old')
@pytest.mark.filterwarnings('ignore:canonicalization and validation of license expression')
def test_dynamic_license_list(tmp_path):
tmp_path.joinpath('pyproject.toml').write_text(textwrap.dedent('''
[build-system]
build-backend = 'mesonpy'
requires = ['meson-python']
[project]
name = 'test'
version = '1.0.0'
dynamic = ['license']
'''), encoding='utf8')
tmp_path.joinpath('meson.build').write_text(textwrap.dedent('''
project('test', license: ['MIT', 'BSD-3-Clause'])
'''), encoding='utf8')
with pytest.raises(pyproject_metadata.ConfigurationError, match='Using a list of strings for the license'):
mesonpy.Project(tmp_path, tmp_path / 'build')
@pytest.mark.skipif(MESON_VERSION < (1, 6, 0), reason='meson too old')
@pytest.mark.filterwarnings('ignore:canonicalization and validation of license expression')
def test_dynamic_license_missing(tmp_path):
tmp_path.joinpath('pyproject.toml').write_text(textwrap.dedent('''
[build-system]
build-backend = 'mesonpy'
requires = ['meson-python']
[project]
name = 'test'
version = '1.0.0'
dynamic = ['license']
'''), encoding='utf8')
tmp_path.joinpath('meson.build').write_text(textwrap.dedent('''
project('test')
'''), encoding='utf8')
with pytest.raises(pyproject_metadata.ConfigurationError, match='Field "license" declared as dynamic but'):
mesonpy.Project(tmp_path, tmp_path / 'build')
@pytest.mark.skipif(MESON_VERSION < (1, 6, 0), reason='meson too old')
@pytest.mark.filterwarnings('ignore:canonicalization and validation of license expression')
def test_dynamic_license_files(tmp_path):
tmp_path.joinpath('pyproject.toml').write_text(textwrap.dedent('''
[build-system]
build-backend = 'mesonpy'
requires = ['meson-python']
[project]
name = 'test'
version = '1.0.0'
dynamic = ['license', 'license-files']
'''), encoding='utf8')
tmp_path.joinpath('meson.build').write_text(textwrap.dedent('''
project('test', license: 'MIT', license_files: ['LICENSE'])
'''), encoding='utf8')
tmp_path.joinpath('LICENSE').write_text('')
p = mesonpy.Project(tmp_path, tmp_path / 'build')
assert metadata(bytes(p._metadata.as_rfc822())) == metadata(textwrap.dedent('''\
Metadata-Version: 2.4
Name: test
Version: 1.0.0
License-Expression: MIT
License-File: LICENSE
'''))
def test_user_args(package_user_args, tmp_path, monkeypatch):
project_run = mesonpy.Project._run
cmds = []
args = []
def wrapper(self, cmd):
# intercept and filter out test arguments and forward the call
if cmd[:2] == ['meson', 'compile']:
# when using meson compile instead of ninja directly, the
# arguments needs to be unmarshalled from the form used to
# pass them to the --ninja-args option
assert cmd[-1].startswith('--ninja-args=')
cmds.append(cmd[:2])
args.append(ast.literal_eval(cmd[-1].split('=')[1]))
elif cmd[:1] == ['meson']:
cmds.append(cmd[:2])
args.append(cmd[2:])
else:
# direct ninja invocation
cmds.append([os.path.basename(cmd[0])])
args.append(cmd[1:])
return project_run(self, [x for x in cmd if not x.startswith(('config-', 'cli-', '--ninja-args'))])
monkeypatch.setattr(mesonpy.Project, '_run', wrapper)
config_settings = {
'dist-args': ('cli-dist',),
'setup-args': ('cli-setup',),
'compile-args': ('cli-compile',),
'install-args': ('cli-install',),
}
with in_git_repo_context():
mesonpy.build_sdist(tmp_path, config_settings)
mesonpy.build_wheel(tmp_path, config_settings)
# check that the right commands are executed, namely that 'meson
# compile' is used on Windows rather than a 'ninja' direct
# invocation.
assert cmds == [
# sdist: calls to 'meson setup' and 'meson dist'
['meson', 'setup'],
['meson', 'dist'],
# wheel: calls to 'meson setup', 'meson compile', and 'meson install'
['meson', 'setup'],
['meson', 'compile'] if sys.platform == 'win32' else ['ninja'],
]
# check that the user options are passed to the invoked commands
expected = [
# sdist: calls to 'meson setup' and 'meson dist'
['config-setup', 'cli-setup'],
['config-dist', 'cli-dist'],
# wheel: calls to 'meson setup', 'meson compile', and 'meson install'
['config-setup', 'cli-setup'],
['config-compile', 'cli-compile'],
['config-install', 'cli-install'],
]
for expected_args, cmd_args in zip(expected, args):
for arg in expected_args:
assert arg in cmd_args
@pytest.mark.parametrize('package', ('top-level', 'meson-args'))
def test_unknown_user_args(package, tmp_path_session):
with pytest.raises(mesonpy.ConfigError):
mesonpy.Project(package_dir / f'unknown-user-args-{package}', tmp_path_session)
def test_install_tags(package_purelib_and_platlib, tmp_path_session):
project = mesonpy.Project(
package_purelib_and_platlib,
tmp_path_session,
meson_args={
'install': ['--tags', 'purelib'],
}
)
assert 'platlib' not in project._manifest
def test_validate_pyproject_config_one():
pyproject_config = tomllib.loads(textwrap.dedent('''
[tool.meson-python.args]
setup = ['-Dfoo=true']
'''))
conf = mesonpy._validate_pyproject_config(pyproject_config)
assert conf['args'] == {'setup': ['-Dfoo=true']}
def test_validate_pyproject_config_all():
pyproject_config = tomllib.loads(textwrap.dedent('''
[tool.meson-python.args]
setup = ['-Dfoo=true']
dist = []
compile = ['-j4']
install = ['--tags=python']
'''))
conf = mesonpy._validate_pyproject_config(pyproject_config)
assert conf['args'] == {
'setup': ['-Dfoo=true'],
'dist': [],
'compile': ['-j4'],
'install': ['--tags=python']}
def test_validate_pyproject_config_unknown():
pyproject_config = tomllib.loads(textwrap.dedent('''
[tool.meson-python.args]
invalid = true
'''))
with pytest.raises(mesonpy.ConfigError, match='Unknown configuration entry "tool.meson-python.args.invalid"'):
mesonpy._validate_pyproject_config(pyproject_config)
def test_validate_pyproject_config_empty():
pyproject_config = tomllib.loads(textwrap.dedent(''))
config = mesonpy._validate_pyproject_config(pyproject_config)
assert config == {}
@pytest.mark.skipif(
sys.version_info < (3, 8),
reason="unittest.mock doesn't support the required APIs for this test",
)
def test_invalid_build_dir(package_pure, tmp_path, mocker):
meson = mocker.spy(mesonpy.Project, '_run')
# configure the project
project = mesonpy.Project(package_pure, tmp_path)
assert len(meson.call_args_list) == 1
assert meson.call_args_list[0].args[1][1] == 'setup'
assert '--reconfigure' not in meson.call_args_list[0].args[1]
project.build()
meson.reset_mock()
# subsequent builds with the same build directory result in a setup --reconfigure
project = mesonpy.Project(package_pure, tmp_path)
assert len(meson.call_args_list) == 1
assert meson.call_args_list[0].args[1][1] == 'setup'
assert '--reconfigure' in meson.call_args_list[0].args[1]
project.build()
meson.reset_mock()
# corrupting the build direcory setup is run again
tmp_path.joinpath('meson-private/coredata.dat').unlink()
project = mesonpy.Project(package_pure, tmp_path)
assert len(meson.call_args_list) == 1
assert meson.call_args_list[0].args[1][1] == 'setup'
assert '--reconfigure' not in meson.call_args_list[0].args[1]
project.build()
meson.reset_mock()
# removing the build directory things should still work
shutil.rmtree(tmp_path)
project = mesonpy.Project(package_pure, tmp_path)
assert len(meson.call_args_list) == 1
assert meson.call_args_list[0].args[1][1] == 'setup'
assert '--reconfigure' not in meson.call_args_list[0].args[1]
project.build()
@pytest.mark.skipif(not os.getenv('CI') or sys.platform != 'win32', reason='requires MSVC')
def test_compiler(venv, package_detect_compiler, tmp_path):
# Check that things are setup properly to use the MSVC compiler on
# Windows. This effectively means running the compilation step
# with 'meson compile' instead of 'ninja' on Windows. Run this
# test only on CI where we know that MSVC is available.
wheel = mesonpy.build_wheel(tmp_path, {'setup-args': ['--vsenv']})
venv.pip('install', os.fspath(tmp_path / wheel))
compiler = venv.python('-c', 'import detect_compiler; print(detect_compiler.compiler())').strip()
assert compiler == 'msvc'
@pytest.mark.skipif(sys.platform != 'darwin', reason='macOS specific test')
@pytest.mark.parametrize('archflags', [
'-arch x86_64',
'-arch arm64',
'-arch arm64 -arch arm64',
])
def test_archflags_envvar_parsing(package_purelib_and_platlib, monkeypatch, archflags):
try:
monkeypatch.setenv('ARCHFLAGS', archflags)
arch = archflags.split()[-1]
with mesonpy._project():
assert mesonpy._tags.Tag().platform.endswith(arch)
finally:
# revert environment variable setting done by the in-process build
os.environ.pop('_PYTHON_HOST_PLATFORM', None)
@pytest.mark.skipif(sys.platform != 'darwin', reason='macOS specific test')
@pytest.mark.parametrize('archflags', [
'-arch arm64 -arch x86_64',
'-arch arm64 -DFOO=1',
])
def test_archflags_envvar_parsing_invalid(package_purelib_and_platlib, monkeypatch, archflags):
try:
monkeypatch.setenv('ARCHFLAGS', archflags)
with pytest.raises(mesonpy.ConfigError):
with mesonpy._project():
pass
finally:
# revert environment variable setting done by the in-process build
os.environ.pop('_PYTHON_HOST_PLATFORM', None)
|