pyodide/pyodide-build/pyodide_build/io.py

239 lines
7.2 KiB
Python

from pathlib import Path
from typing import Any, Literal
import pydantic
from pydantic import BaseModel, Field
class _PackageSpec(BaseModel):
name: str
version: str
top_level: list[str] = Field([], alias="top-level")
tag: list[str] = Field([])
disabled: bool = Field(False, alias="_disabled")
class Config:
extra = pydantic.Extra.forbid
class _SourceSpec(BaseModel):
url: str | None = None
extract_dir: str | None = None
path: Path | None = None
sha256: str | None = None
patches: list[str] = []
extras: list[tuple[str, str]] = []
class Config:
extra = pydantic.Extra.forbid
@pydantic.root_validator
def _check_url_has_hash(cls, values: dict[str, Any]) -> dict[str, Any]:
if values["url"] is not None and values["sha256"] is None:
raise ValueError(
"If source is downloaded from url, it must have a 'source/sha256' hash."
)
return values
@pydantic.root_validator
def _check_in_tree_url(cls, values: dict[str, Any]) -> dict[str, Any]:
in_tree = values["path"] is not None
from_url = values["url"] is not None
# cpython_modules is a special case, it is not in the tree
# TODO: just copy the file into the tree?
# if not (in_tree or from_url):
# raise ValueError("Source section should have a 'url' or 'path' key")
if in_tree and from_url:
raise ValueError(
"Source section should not have both a 'url' and a 'path' key"
)
return values
@pydantic.root_validator
def _check_patches_extra(cls, values: dict[str, Any]) -> dict[str, Any]:
patches = values["patches"]
extras = values["extras"]
in_tree = values["path"] is not None
from_url = values["url"] is not None
url_is_wheel = from_url and values["url"].endswith(".whl")
if in_tree and (patches or extras):
raise ValueError(
"If source is in tree, 'source/patches' and 'source/extras' keys "
"are not allowed"
)
if url_is_wheel and (patches or extras):
raise ValueError(
"If source is a wheel, 'source/patches' and 'source/extras' "
"keys are not allowed"
)
return values
_ExportTypes = Literal["pyinit", "requested", "whole_archive"]
_BuildSpecExports = _ExportTypes | list[str]
_BuildSpecTypes = Literal[
"package", "static_library", "shared_library", "cpython_module"
]
class _BuildSpec(BaseModel):
exports: _BuildSpecExports = "pyinit"
backend_flags: str = Field("", alias="backend-flags")
cflags: str = ""
cxxflags: str = ""
ldflags: str = ""
package_type: _BuildSpecTypes = Field("package", alias="type")
cross_script: str | None = Field(None, alias="cross-script")
script: str | None = None
post: str | None = None
unvendor_tests: bool = Field(True, alias="unvendor-tests")
vendor_sharedlib: bool = Field(False, alias="vendor-sharedlib")
cross_build_env: bool = Field(False, alias="cross-build-env")
cross_build_files: list[str] = Field([], alias="cross-build-files")
class Config:
extra = pydantic.Extra.forbid
@pydantic.root_validator
def _check_config(cls, values: dict[str, Any]) -> dict[str, Any]:
static_library = values["package_type"] == "static_library"
shared_library = values["package_type"] == "shared_library"
cpython_module = values["package_type"] == "cpython_module"
if not (static_library or shared_library or cpython_module):
return values
allowed_keys = {
"package_type",
"script",
"exports",
"unvendor_tests",
}
typ = values["package_type"]
for key, val in values.items():
if val and key not in allowed_keys:
raise ValueError(
f"If building a {typ}, 'build/{key}' key is not allowed."
)
return values
class _RequirementsSpec(BaseModel):
run: list[str] = []
host: list[str] = []
executable: list[str] = []
class Config:
extra = pydantic.Extra.forbid
class _TestSpec(BaseModel):
imports: list[str] = []
class Config:
extra = pydantic.Extra.forbid
class _AboutSpec(BaseModel):
home: str | None = None
PyPI: str | None = None
summary: str | None = None
license: str | None = None
class Config:
extra = pydantic.Extra.forbid
class MetaConfig(BaseModel):
package: _PackageSpec
source: _SourceSpec = _SourceSpec()
build: _BuildSpec = _BuildSpec()
requirements: _RequirementsSpec = _RequirementsSpec()
test: _TestSpec = _TestSpec()
about: _AboutSpec = _AboutSpec()
class Config:
extra = pydantic.Extra.forbid
@classmethod
def from_yaml(cls, path: Path) -> "MetaConfig":
"""Load the meta.yaml from a path
Parameters
----------
path
path to the meta.yaml file
"""
import yaml
stream = path.read_bytes()
config_raw = yaml.safe_load(stream)
config = cls(**config_raw)
if config.source.path:
config.source.path = path.parent / config.source.path
return config
def to_yaml(self, path: Path) -> None:
"""Serialize the configuration to meta.yaml file
Parameters
----------
path
path to the meta.yaml file
"""
import yaml
with open(path, "w") as f:
yaml.dump(self.dict(by_alias=True, exclude_unset=True), f)
@pydantic.root_validator
def _check_wheel_host_requirements(cls, values: dict[str, Any]) -> dict[str, Any]:
"""Check that if sources is a wheel it shouldn't have host dependencies."""
if "source" not in values:
raise ValueError(
'either "path" or "url" must be provided in the "source" section'
)
source_url = values["source"].url
requirements_host = values["requirements"].host
if source_url is not None and source_url.endswith(".whl"):
if len(requirements_host):
raise ValueError(
f"When source -> url is a wheel ({source_url}) the package cannot have host "
f"dependencies. Found {requirements_host}"
)
allowed_keys = {
"post",
"unvendor-tests",
# Note here names are with "_", after alias conversion
"cross_build_env",
"cross_build_files",
"exports",
"unvendor_tests",
"package_type",
}
for key, val in values["build"].dict().items():
if val and key not in allowed_keys:
raise ValueError(
f"If source is a wheel, 'build/{key}' key is not allowed"
)
return values
def is_rust_package(self) -> bool:
"""
Check if a package requires rust toolchain to build.
"""
return any(
q in self.requirements.executable for q in ("rustc", "cargo", "rustup")
)