Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Declare ext_modules and libraries in pyproject.toml #262

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/poetry/core/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,27 @@ def configure_package(
if "urls" in config:
package.custom_urls = config["urls"]

include_dirs: set[str] = set()

# C library build info
if "libraries" in config:
package.libraries = config["libraries"]
for _, build_info in config["libraries"].items():
if "include_dirs" in build_info:
include_dirs.update(build_info["include_dirs"])

# Extension module build info
if "ext_modules" in config:
package.ext_modules = config["ext_modules"]
for _, build_info in config["ext_modules"].items():
if "include_dirs" in build_info:
include_dirs.update(build_info["include_dirs"])

if include_dirs:
package.include = package.include or []
for include in include_dirs:
package.include.append({"path": include, "format": ["sdist"]})

return package

@classmethod
Expand Down Expand Up @@ -435,6 +456,12 @@ def validate(
f" {', '.join(sorted(readme_types))}"
)

if "build" in config and ("libraries" in config or "ext_modules" in config):
result["errors"].append(
"Extension modules and C libraries may not be declared in"
" pyproject.toml when using a build script"
)

return result

@classmethod
Expand Down
157 changes: 157 additions & 0 deletions src/poetry/core/json/schemas/poetry-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,20 @@
"description": "The full url of the custom url."
}
}
},
"libraries": {
"type": "object",
"description": "A set of project libraries to compile against",
"additionalProperties": {
"$ref": "#/definitions/library"
}
},
"ext_modules": {
"type": "object",
"description": "A set of project extension modules to compile and include",
"additionalProperties": {
"$ref": "#/definitions/ext-module"
}
}
},
"definitions": {
Expand Down Expand Up @@ -650,6 +664,149 @@
"$ref": "#/definitions/build-config"
}
]
},
"library": {
"type": "object",
"description": "C library build info",
"properties": {
"sources": {
"type": "array",
"items": {
"type": "string"
},
"minItems": 1,
"description": "A list of source filenames or globs, relative to the distribution root (where pyproject.toml is located), in Unix form (slash-separated) for portability. Source files may be C, C++, SWIG (.i), platform-specific resource files, or whatever else is recognized by the build_ext command as source for a Python extension."
},
"include_dirs": {
"type": "array",
"items": {
"type": "string"
},
"description": "A list of directories to search for C/C++ header files (in Unix form for portability)"
},
"macros": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": "string"
},
"minLength": 2,
"maxLength": 2
},
"description": "A list of macros to define; each macro is defined using a 2-tuple (name, value), where value is either the string to define it to or an empty string to define it without a particular value (equivalent of #define FOO in source or -DFOO on Unix C compiler command line)"
}
},
"required": [
"sources"
]
},
"ext-module": {
"type": "object",
"description": "Extension module build info",
"properties": {
"sources": {
"type": "array",
"items": {
"type": "string"
},
"minItems": 1,
"description": "A list of source filenames or globs, relative to the distribution root (where pyproject.toml is located), in Unix form (slash-separated) for portability. Source files may be C, C++, SWIG (.i), platform-specific resource files, or whatever else is recognized by the build_ext command as source for a Python extension."
},
"include_dirs": {
"type": "array",
"items": {
"type": "string"
},
"description": "A list of directories to search for C/C++ header files (in Unix form for portability)"
},
"define_macros": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": "string"
},
"minLength": 2,
"maxLength": 2
},
"description": "A list of macros to define; each macro is defined using a 2-tuple (name, value), where value is either the string to define it to or an empty string to define it without a particular value (equivalent of #define FOO in source or -DFOO on Unix C compiler command line)"
},
"undef_macros": {
"type": "array",
"items": {
"type": "string"
},
"description": "A list of macros to undefine explicitly"
},
"library_dirs": {
"type": "array",
"items": {
"type": "string"
},
"description": "A list of directories to search for C/C++ libraries at link time"
},
"libraries": {
"type": "array",
"items": {
"type": "string"
},
"description": "A list of library names (not filenames or paths) to link against"
},
"runtime_library_dirs": {
"type": "array",
"items": {
"type": "string"
},
"description": "A list of directories to search for C/C++ libraries at run time (for shared extensions, this is when the extension is loaded)"
},
"extra_objects": {
"type": "array",
"items": {
"type": "string"
},
"description": "A list of paths or globs of extra files to link with (eg. object files not implied by ‘sources’, static library that must be explicitly specified, binary resource files, etc.)"
},
"extra_compile_args": {
"type": "array",
"items": {
"type": "string"
},
"description": "Any extra platform- and compiler-specific information to use when compiling the source files in ‘sources’. For platforms and compilers where a command line makes sense, this is typically a list of command-line arguments, but for other platforms it could be anything."
},
"extra_link_args": {
"type": "array",
"items": {
"type": "string"
},
"description": "Any extra platform- and compiler-specific information to use when linking object files together to create the extension (or to create a new static Python interpreter). Similar interpretation as for ‘extra_compile_args’."
},
"export_symbols": {
"type": "array",
"items": {
"type": "string"
},
"description": "A list of symbols to be exported from a shared extension. Not used on all platforms, and not generally necessary for Python extensions, which typically export exactly one symbol: `init` + extension_name."
},
"depends": {
"type": "array",
"items": {
"type": "string"
},
"description": "A list of paths or globs of files that the extension depends on"
},
"language": {
"type": "string",
"description": "The extension language (i.e. `'c'`, `'c++'`, `'objc'`). Will be detected from the source extensions if not provided."
},
"optional": {
"type": "boolean",
"description": "Specifies that a build failure in the extension should not abort the build process, but simply skip the extension."
}
},
"required": [
"sources"
]
}
}
}
135 changes: 130 additions & 5 deletions src/poetry/core/masonry/builders/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from collections import defaultdict
from pathlib import Path
from typing import TYPE_CHECKING
from typing import Any


if TYPE_CHECKING:
Expand Down Expand Up @@ -320,22 +321,26 @@ def convert_script_files(self) -> list[Path]:
if isinstance(specification, dict) and specification.get("type") == "file":
source = specification["reference"]

if Path(source).is_absolute():
try:
_ = self.get_source_paths(name, [source])[0]
except IndexError:
raise RuntimeError(
f"{source} in {name} is an absolute path. Expected relative"
" path."
f"{source} in script/module specification ({name}) is not"
" found."
)

abs_path = Path.joinpath(self._path, source)

if not abs_path.exists():
raise RuntimeError(
f"{abs_path} in script specification ({name}) is not found."
f"{abs_path} in script/module specification ({name}) is not"
" found."
)

if not abs_path.is_file():
raise RuntimeError(
f"{abs_path} in script specification ({name}) is not a file."
f"{abs_path} in script/module specification ({name}) is not a"
" file."
)

script_files.append(abs_path)
Expand All @@ -353,6 +358,126 @@ def convert_author(cls, author: str) -> dict[str, str]:

return {"name": name, "email": email}

def convert_libraries(self) -> list[tuple[str, dict[str, Any]]]:
"""
Convert library build info to required format for setuptools.setup().
Expand globs and validate existence of source files.
"""
libraries = []

for name, build_info in self._poetry.local_config.get("libraries", {}).items():
build_info = dict(build_info)
build_info["sources"] = [
str(src)
for src in self.get_source_paths(
name, build_info["sources"], expand_globs=True
)
]
if len(build_info["sources"]) == 0:
raise RuntimeError(f"No valid sources specified for library {name}")

if "macros" in build_info:
build_info["macros"] = [
tuple(macro_def) for macro_def in build_info["macros"]
]

libraries.append((name, build_info))

return libraries

def convert_ext_modules(self) -> list[tuple[str, dict[str, Any]]]:
"""
Convert extension module build info to arguments for initializing setuptools.Extension objects.
Expand globs and validate existence of source and other files.
"""
ext_modules = []

for name, build_info in self._poetry.local_config.get(
"ext_modules", {}
).items():
build_info = dict(build_info)
build_info["sources"] = [
str(src)
for src in self.get_source_paths(
name, build_info["sources"], expand_globs=True
)
]
if len(build_info["sources"]) == 0:
raise RuntimeError(
f"No valid sources specified for extension module {name}"
)

if "define_macros" in build_info:
build_info["define_macros"] = [
tuple(macro_def) for macro_def in build_info["define_macros"]
]

if "extra_objects" in build_info:
build_info["extra_objects"] = [
str(src)
for src in self.get_source_paths(
name, build_info["extra_objects"], expand_globs=True
)
]

if "depends" in build_info:
build_info["depends"] = [
str(src)
for src in self.get_source_paths(
name, build_info["depends"], expand_globs=True
)
]

ext_modules.append((name, build_info))

return ext_modules

def get_source_paths(
self, name: str, paths: list[str], expand_globs: bool = False
) -> list[Path]:
"""
Get source(s) specified as a list of relative paths within the project

:param name: Module name
:param paths: List of paths to source file/dir
:param expand_globs: Whether to expand globs, if present
:return: Absolute paths of source files/dirs
"""
source_paths: set[Path] = set()

# Resolve absolute paths
for path in paths:
if expand_globs:
try:
source_paths.update(self._path.glob(path))
except NotImplementedError:
raise RuntimeError(
f"{path} in {name} is an absolute path. Expected relative path."
)
else:
source = Path(path)
if source.is_absolute():
raise RuntimeError(
f"{source} in {name} is an absolute path. Expected relative"
" path."
)
source_paths.add(Path.joinpath(self._path, source))

sources: list[Path] = []

for src in source_paths:
if not src.exists():
continue

if not src.is_file():
raise RuntimeError(
f"{src} in script/module specification ({name}) is not a file."
)

sources.append(src)

return sources


class BuildIncludeFile:
def __init__(
Expand Down
Loading