From 7cf7bb2443dcecc9a25509b8f0f084379fe6282b Mon Sep 17 00:00:00 2001 From: Pieter Pas Date: Mon, 24 Oct 2022 02:17:14 +0200 Subject: [PATCH] Default ABI, version, arch for cross-compilation, allow absolute or relative toolchain file paths, differences between paths relative to config file or project folder. --- docs/Config.md | 16 +++--- src/py_build_cmake/build.py | 22 +++++---- src/py_build_cmake/config_options.py | 66 +++++++++++++++++++------ src/py_build_cmake/help/__main__.py | 16 +++++- src/py_build_cmake/pyproject_options.py | 23 +++++---- test/test_configoptions.py | 49 +++++++++--------- 6 files changed, 126 insertions(+), 66 deletions(-) diff --git a/docs/Config.md b/docs/Config.md index beb6b41..e06764b 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -10,7 +10,7 @@ Defines the name and the directory of the module to package. | Option | Description | Type | Default | |--------|-------------|------|---------| | `name` | Import name in Python (can be different from the name on PyPI, which is defined in the [project] section). | string | `/pyproject.toml/project/name` | -| `directory` | Directory containing the Python package. | path | `'.'` | +| `directory` | Directory containing the Python package.
Relative to project directory. | path | `'.'` | ## sdist Specifies the files that should be included in the source distribution for this package. @@ -29,8 +29,8 @@ Defines how to build the project to package. If omitted, py-build-cmake will pro | `build_type` | Build type passed to the configuration step, as -DCMAKE\_BUILD\_TYPE=<?>.
For example: `build_type = "RelWithDebInfo"` | string | `none` | | `config` | Configuration type passed to the build and install steps, as --config <?>. You can specify either a single string, or a list of strings. If a multi-config generator is used, all configurations in this list will be included in the package. | list | `build_type` | | `generator` | CMake generator to use, passed to the configuration step, as -G <?>. | string | `none` | -| `source_path` | Folder containing CMakeLists.txt. | path | `'.'` | -| `build_path` | CMake build and cache folder. | path | `'.py-build-cmake_cache'` | +| `source_path` | Folder containing CMakeLists.txt.
Relative to project directory. | path | `'.'` | +| `build_path` | CMake build and cache folder.
Relative. | path | `'.py-build-cmake_cache'` | | `options` | Extra options passed to the configuration step, as -D<option>=<value>.
For example: `options = {"WITH_FEATURE_X" = "On"}` | dict | `{}` | | `args` | Extra arguments passed to the configuration step.
For example: `args = ["--debug-find", "-Wdev"]` | list | `[]` | | `build_args` | Extra arguments passed to the build step.
For example: `build_args = ["-j"]` | list | `[]` | @@ -78,11 +78,11 @@ Causes py-build-cmake to cross-compile the project. | Option | Description | Type | Default | |--------|-------------|------|---------| -| `implementation` | Identifier for the Python implementation.
For example: `implementation = 'cp' # CPython` | string | `required` | -| `version` | Python version, major and minor, without dots.
For example: `version = '310' # 3.10` | string | `required` | -| `abi` | Python ABI.
For example: `abi = 'cp310'` | string | `required` | -| `arch` | Operating system and architecture (no dots or dashes, only underscores, all lowercase).
For example: `arch = 'linux_x86_64'` | string | `required` | -| `toolchain_file` | CMake toolchain file to use. | path | `required` | +| `implementation` | Identifier for the Python implementation.
For example: `implementation = 'cp' # CPython` | string | `same as current interpreter` | +| `version` | Python version, major and minor, without dots.
For example: `version = '310' # 3.10` | string | `same as current interpreter` | +| `abi` | Python ABI.
For example: `abi = 'cp310'` | string | `same as current interpreter` | +| `arch` | Operating system and architecture (no dots or dashes, only underscores, all lowercase).
For example: `arch = 'linux_x86_64'` | string | `same as current interpreter` | +| `toolchain_file` | CMake toolchain file to use.
Absolute or relative to current configuration file. | filepath | `required` | | `copy_from_native_build` | If set, this will cause a native version of the CMake project to be built and installed in a temporary directory first, and the files in this list will be copied to the final cross-compiled package. This is useful if you need binary utilities that run on the build system while cross-compiling, or for things like stubs for extension modules that cannot be generated while cross-compiling.
May include the '\*' wildcard (but not '\*\*' for recursive patterns). | list | `none` | | `sdist` | Override sdist options when cross-compiling.
Inherits from: `/pyproject.toml/tool/py-build-cmake/sdist` | | `none` | | `cmake` | Override CMake options when cross-compiling.
Inherits from: `/pyproject.toml/tool/py-build-cmake/cmake` | | `none` | diff --git a/src/py_build_cmake/build.py b/src/py_build_cmake/build.py index 3e2011d..4edbcf6 100644 --- a/src/py_build_cmake/build.py +++ b/src/py_build_cmake/build.py @@ -524,17 +524,9 @@ def run(self, *args, **kwargs): pprint(kwargs) return sp_run(*args, **kwargs) - @staticmethod - def get_cross_tags(crosscfg): - """Get the PEP 425 tags to use when cross-compiling.""" - return { - 'pyver': [crosscfg['implementation'] + crosscfg['version']], - 'abi': [crosscfg['abi']], - 'arch': [crosscfg['arch']], - } - @staticmethod def get_native_tags(): + """Get the PEP 425 tags for the current platform.""" from .tags import get_python_tag, get_abi_tag, get_platform_tag return { 'pyver': [get_python_tag()], @@ -542,6 +534,18 @@ def get_native_tags(): 'arch': [get_platform_tag()], } + @staticmethod + def get_cross_tags(crosscfg): + """Get the PEP 425 tags to use when cross-compiling.""" + tags = _BuildBackend.get_native_tags() + if 'implementation' in crosscfg and 'version' in crosscfg: + tags['pyver'] = [crosscfg['implementation'] + crosscfg['version']] + if 'abi' in crosscfg: + tags['abi'] = [crosscfg['abi']] + if 'arch' in crosscfg: + tags['arch'] = [crosscfg['arch']] + return tags + @staticmethod def get_build_config_name(cross_cfg): """Get a string representing the Python version, ABI and architecture, diff --git a/src/py_build_cmake/config_options.py b/src/py_build_cmake/config_options.py index 6f66447..6cf456b 100644 --- a/src/py_build_cmake/config_options.py +++ b/src/py_build_cmake/config_options.py @@ -148,13 +148,16 @@ def get_name(self): class NoDefaultValue(DefaultValue): + def __init__(self, name = 'none'): + self.name = name + def get_default(self, rootopts: 'ConfigOption', opt: 'ConfigOption', cfg: ConfigNode, cfgpath: ConfPath, optpath: ConfPath) -> Optional[DefaultValueWrapper]: return None def get_name(self): - return 'none' + return self.name class MissingDefaultError(ConfigError): @@ -488,6 +491,16 @@ def verify(self, rootopts: 'ConfigOption', cfg: ConfigNode, f'{str}, not {type(cfg[cfgpath].value)}') +class RelativeToCurrentConfig: + description = 'current configuration file' + + +@dataclass +class RelativeToProject: + project_path: Path + description: str = 'project directory' + + class PathConfigOption(StrConfigOption): def __init__(self, @@ -497,32 +510,57 @@ def __init__(self, default: DefaultValue = NoDefaultValue(), must_exist: bool = True, expected_contents: List[str] = [], - base_path: Optional[Path] = None): + base_path: Optional[Union[RelativeToProject, + RelativeToCurrentConfig]] = None, + allow_abs: bool = False, + is_folder: bool = True): super().__init__(name, description, example, default) - self.must_exist = must_exist + self.must_exist = must_exist or bool(expected_contents) self.expected_contents = expected_contents self.base_path = base_path + self.allow_abs = allow_abs + self.is_folder = is_folder def get_typename(self): - return 'path' + return 'path' if self.is_folder else 'filepath' def check_path(self, cfg: ConfigNode, cfgpath): - path = cfg[cfgpath].value = os.path.normpath(cfg[cfgpath].value) + path = os.path.normpath(cfg[cfgpath].value) + # Absolute or relative path? if os.path.isabs(path): - raise ConfigError(f'{pth2str(cfgpath)} must be a relative path') - if self.base_path is not None: - abspath = self.base_path / path - if self.must_exist and not os.path.exists(abspath): - raise ConfigError(f'{pth2str(cfgpath)}: {str(abspath)} ' - f'does not exist') + # Absolute path + if not self.allow_abs: + raise ConfigError(f'{pth2str(cfgpath)}: "{str(path)}" ' + f'must be a relative path') + else: + # Relative path + if isinstance(self.base_path, RelativeToCurrentConfig): + path = os.path.join(os.path.dirname(cfgpath[0]), path) + elif isinstance(self.base_path, RelativeToProject): + path = os.path.join(self.base_path.project_path, path) + else: + assert self.base_path is None + # Does the path exist? + if self.must_exist: + assert os.path.isabs(path) + if not os.path.exists(path): + raise ConfigError(f'{pth2str(cfgpath)}: "{str(path)}" ' + f'does not exist') + if self.is_folder != os.path.isdir(path): + type_ = 'directory' if self.is_folder else 'file' + raise ConfigError(f'{pth2str(cfgpath)}: "{str(path)}" ' + f'should be a {type_}') + # Are any of the required contents missing? missing = [ sub for sub in self.expected_contents - if not os.path.exists(os.path.join(abspath, sub)) + if not os.path.exists(os.path.join(path, sub)) ] if missing: missingstr = '", "'.join(missing) - raise ConfigError(f'{pth2str(cfgpath)} does not contain ' - f'required files or folders "{missingstr}"') + raise ConfigError(f'{pth2str(cfgpath)}: "{str(path)}" ' + f'does not contain the following ' + f'required files or folders: "{missingstr}"') + cfg[cfgpath].value = os.path.normpath(path) def verify(self, rootopts: 'ConfigOption', cfg: ConfigNode, cfgpath: ConfPath): diff --git a/src/py_build_cmake/help/__main__.py b/src/py_build_cmake/help/__main__.py index 8bc21c6..6ac7340 100644 --- a/src/py_build_cmake/help/__main__.py +++ b/src/py_build_cmake/help/__main__.py @@ -1,10 +1,11 @@ import html import itertools +from pathlib import Path import shutil import sys import textwrap -from py_build_cmake.config_options import ConfigOption, NoDefaultValue, RefDefaultValue, RequiredValue, pth, pth2str +from py_build_cmake.config_options import ConfigOption, NoDefaultValue, PathConfigOption, RefDefaultValue, RequiredValue, pth, pth2str from py_build_cmake.pyproject_options import get_options @@ -45,6 +46,8 @@ def help_print_md(pbc_opts: ConfigOption): def _get_full_description(vv: ConfigOption): descr = _md_escape(vv.description) + if isinstance(vv, PathConfigOption): + descr += '
' + _describe_path_option(vv).capitalize() + '.' if vv.inherit_from: descr += '
Inherits from: `/' + pth2str(vv.inherit_from) + '`' if vv.example: @@ -78,6 +81,8 @@ def recursive_help_print(opt: ConfigOption, level=0): typename = v.get_typename() if typename is not None: headerfields += [typename] + if isinstance(v, PathConfigOption): + headerfields += [_describe_path_option(v)] is_required = isinstance(v.default, RequiredValue) if is_required: headerfields += ['required'] @@ -94,6 +99,13 @@ def recursive_help_print(opt: ConfigOption, level=0): print(textwrap.indent('Default: ' + default, indent + ' ')) +def _describe_path_option(v: PathConfigOption): + t = 'absolute or relative' if v.allow_abs else 'relative' + if v.base_path is not None: + t += ' to ' + v.base_path.description + return t + + def _print_usage(): print( textwrap.dedent("""\ @@ -112,7 +124,7 @@ def _print_usage(): def main(): - opts = get_options() + opts = get_options(Path('/')) help_pth = pth('pyproject.toml/tool/py-build-cmake') help_opt = {'-h', '-?', '--help', 'h', 'help', '?'} if len(sys.argv) == 2 and sys.argv[1] == 'md': diff --git a/src/py_build_cmake/pyproject_options.py b/src/py_build_cmake/pyproject_options.py index d7359ee..eefc100 100644 --- a/src/py_build_cmake/pyproject_options.py +++ b/src/py_build_cmake/pyproject_options.py @@ -9,7 +9,7 @@ def get_cross_path(): return pth('pyproject.toml/tool/py-build-cmake/cross') -def get_options(config_path: Optional[Path] = None): +def get_options(project_path: Path, *, test: bool = False): root = ConfigOption("root") pyproject = root.insert(UncheckedConfigOption("pyproject.toml")) project = pyproject.insert(UncheckedConfigOption('project')) @@ -41,7 +41,8 @@ def get_options(config_path: Optional[Path] = None): PathConfigOption('directory', "Directory containing the Python package.", default=DefaultValueValue("."), - base_path=config_path), + base_path=RelativeToProject(project_path), + must_exist=not test), ]) # [tool.py-build-cmake.sdist] @@ -101,8 +102,9 @@ def get_options(config_path: Optional[Path] = None): PathConfigOption('source_path', "Folder containing CMakeLists.txt.", default=DefaultValueValue("."), - expected_contents=["CMakeLists.txt"], - base_path=config_path), + expected_contents=[] if test else ["CMakeLists.txt"], + base_path=RelativeToProject(project_path), + must_exist=not test), PathConfigOption('build_path', "CMake build and cache folder.", default=DefaultValueValue('.py-build-cmake_cache'), @@ -197,24 +199,27 @@ def get_options(config_path: Optional[Path] = None): StrConfigOption('implementation', "Identifier for the Python implementation.", "implementation = 'cp' # CPython", - default=RequiredValue()), + default=NoDefaultValue('same as current interpreter')), StrConfigOption('version', "Python version, major and minor, without dots.", "version = '310' # 3.10", - default=RequiredValue()), + default=NoDefaultValue('same as current interpreter')), StrConfigOption('abi', "Python ABI.", "abi = 'cp310'", - default=RequiredValue()), + default=NoDefaultValue('same as current interpreter')), StrConfigOption('arch', "Operating system and architecture (no dots or " "dashes, only underscores, all lowercase).", "arch = 'linux_x86_64'", - default=RequiredValue()), + default=NoDefaultValue('same as current interpreter')), PathConfigOption('toolchain_file', "CMake toolchain file to use.", default=RequiredValue(), - base_path=config_path), + base_path=RelativeToCurrentConfig(), + must_exist=not test, + allow_abs=True, + is_folder=False), ListOfStrConfigOption('copy_from_native_build', "If set, this will cause a native version of the " "CMake project to be built and installed in a " diff --git a/test/test_configoptions.py b/test/test_configoptions.py index e4def83..8eaf48e 100644 --- a/test/test_configoptions.py +++ b/test/test_configoptions.py @@ -1,3 +1,4 @@ +from pathlib import Path from pprint import pprint import pytest @@ -532,7 +533,7 @@ def test_joinpth(): def test_real_config_inherit_cross_cmake(): - opts = get_options() + opts = get_options(Path('/project'), test=True) d = { "pyproject.toml": { "project": { @@ -595,7 +596,7 @@ def test_real_config_inherit_cross_cmake(): "cmake": { "build_type": "Release", "generator": "Ninja", - "source_path": "src", + "source_path": "/project/src", "args": ["arg1", "arg2"], "env": { "foo": "bar" @@ -610,7 +611,7 @@ def test_real_config_inherit_cross_cmake(): "cmake": { "build_type": "RelWithDebInfo", "generator": "Unix Makefiles", - "source_path": "src", + "source_path": "/project/src", "args": ["arg1", "arg2", "arg3", "arg4"], "env": { "foo": "bar", @@ -622,7 +623,7 @@ def test_real_config_inherit_cross_cmake(): "cmake": { "build_type": "Release", "generator": "Ninja", - "source_path": "src", + "source_path": "/project/src", "args": ["arg1", "arg2"], "env": { "foo": "bar" @@ -634,7 +635,7 @@ def test_real_config_inherit_cross_cmake(): "cmake": { "build_type": "Release", "generator": "Ninja", - "source_path": "src", + "source_path": "/project/src", "args": ["arg1", "arg2"], "env": { "foo": "bar" @@ -646,7 +647,7 @@ def test_real_config_inherit_cross_cmake(): "cmake": { "build_type": "Release", "generator": "Ninja", - "source_path": "src", + "source_path": "/project/src", "args": ["arg1", "arg2"], "env": { "foo": "bar" @@ -670,7 +671,7 @@ def test_real_config_inherit_cross_cmake(): "py-build-cmake": { "module": { "name": "foobar", - "directory": ".", + "directory": "/project", }, "sdist": { "include": [], @@ -680,7 +681,7 @@ def test_real_config_inherit_cross_cmake(): "build_type": "Release", "config": ["Release"], "generator": "Ninja", - "source_path": "src", + "source_path": "/project/src", "build_path": ".py-build-cmake_cache", "options": {}, "args": ["arg1", "arg2"], @@ -706,7 +707,7 @@ def test_real_config_inherit_cross_cmake(): "build_type": "RelWithDebInfo", "config": ["RelWithDebInfo"], "generator": "Unix Makefiles", - "source_path": "src", + "source_path": "/project/src", "build_path": ".py-build-cmake_cache", "options": {}, "args": ["arg1", "arg2", "arg3", "arg4"], @@ -729,7 +730,7 @@ def test_real_config_inherit_cross_cmake(): "build_type": "Release", "config": ["Release"], "generator": "Ninja", - "source_path": "src", + "source_path": "/project/src", "build_path": ".py-build-cmake_cache", "options": {}, "args": ["arg1", "arg2"], @@ -751,7 +752,7 @@ def test_real_config_inherit_cross_cmake(): "build_type": "Release", "config": ["Release"], "generator": "Ninja", - "source_path": "src", + "source_path": "/project/src", "build_path": ".py-build-cmake_cache", "options": {}, "args": ["arg1", "arg2"], @@ -773,7 +774,7 @@ def test_real_config_inherit_cross_cmake(): "build_type": "Release", "config": ["Release"], "generator": "Ninja", - "source_path": "src", + "source_path": "/project/src", "build_path": ".py-build-cmake_cache", "options": {}, "args": ["arg1", "arg2"], @@ -793,7 +794,7 @@ def test_real_config_inherit_cross_cmake(): def test_real_config_no_cross(): - opts = get_options() + opts = get_options(Path('/project'), test=True) d = { "pyproject.toml": { "project": { @@ -841,7 +842,7 @@ def test_real_config_no_cross(): "py-build-cmake": { "module": { "name": "foobar", - "directory": ".", + "directory": "/project", }, "sdist": { "include": [], @@ -851,7 +852,7 @@ def test_real_config_no_cross(): "build_type": "Release", "config": ["Release"], "generator": "Ninja", - "source_path": "src", + "source_path": "/project/src", "build_path": ".py-build-cmake_cache", "options": {}, "args": ["arg1", "arg2"], @@ -872,7 +873,7 @@ def test_real_config_no_cross(): "build_type": "Release", "config": ["Release"], "generator": "Ninja", - "source_path": "src", + "source_path": "/project/src", "build_path": ".py-build-cmake_cache", "options": {}, "args": ["arg1", "arg2"], @@ -894,7 +895,7 @@ def test_real_config_no_cross(): "build_type": "Release", "config": ["Release"], "generator": "Ninja", - "source_path": "src", + "source_path": "/project/src", "build_path": ".py-build-cmake_cache", "options": {}, "args": ["arg1", "arg2"], @@ -916,7 +917,7 @@ def test_real_config_no_cross(): "build_type": "Release", "config": ["Release"], "generator": "Ninja", - "source_path": "src", + "source_path": "/project/src", "build_path": ".py-build-cmake_cache", "options": {}, "args": ["arg1", "arg2"], @@ -936,7 +937,7 @@ def test_real_config_no_cross(): def test_real_config_no_cmake(): - opts = get_options() + opts = get_options(Path('/project'), test=True) d = { "pyproject.toml": { "project": { @@ -964,7 +965,7 @@ def test_real_config_no_cmake(): "py-build-cmake": { "module": { "name": "foobar", - "directory": ".", + "directory": "/project", }, "sdist": { "include": [], @@ -995,7 +996,7 @@ def test_real_config_no_cmake(): def test_real_config_local_override(): - opts = get_options() + opts = get_options(Path('/project'), test=True) d = { "pyproject.toml": { "project": { @@ -1029,7 +1030,7 @@ def test_real_config_local_override(): "py-build-cmake": { "module": { "name": "foobar", - "directory": ".", + "directory": "/project", }, "sdist": { "include": ["somefile*"], @@ -1065,7 +1066,7 @@ def test_real_config_local_override(): def test_real_config_local_override_windows(): - opts = get_options() + opts = get_options(Path('/project'), test=True) d = { "pyproject.toml": { "project": { @@ -1103,7 +1104,7 @@ def test_real_config_local_override_windows(): "py-build-cmake": { "module": { "name": "foobar", - "directory": ".", + "directory": "/project", }, "sdist": { "include": [],