mirror of https://github.com/pyodide/pyodide.git
160 lines
5.1 KiB
Python
160 lines
5.1 KiB
Python
import os
|
|
import shutil
|
|
import sys
|
|
import traceback
|
|
from collections.abc import Mapping
|
|
from itertools import chain
|
|
from pathlib import Path
|
|
|
|
from build import BuildBackendException, ConfigSettingsType, ProjectBuilder
|
|
from build.__main__ import (
|
|
_STYLES,
|
|
_error,
|
|
_handle_build_error,
|
|
_IsolatedEnvBuilder,
|
|
_ProjectBuilder,
|
|
)
|
|
from build.env import IsolatedEnv
|
|
from packaging.requirements import Requirement
|
|
|
|
from .common import (
|
|
get_hostsitepackages,
|
|
get_pyversion,
|
|
get_unisolated_packages,
|
|
replace_env,
|
|
)
|
|
|
|
AVOIDED_REQUIREMENTS = [
|
|
# We don't want to install cmake Python package inside the isolated env as it will shadow
|
|
# the pywasmcross cmake wrapper.
|
|
"cmake",
|
|
]
|
|
|
|
|
|
def symlink_unisolated_packages(env: IsolatedEnv) -> None:
|
|
pyversion = get_pyversion()
|
|
site_packages_path = f"lib/{pyversion}/site-packages"
|
|
env_site_packages = Path(env.path) / site_packages_path # type: ignore[attr-defined]
|
|
sysconfigdata_name = os.environ["SYSCONFIG_NAME"]
|
|
sysconfigdata_path = (
|
|
Path(os.environ["TARGETINSTALLDIR"]) / f"sysconfigdata/{sysconfigdata_name}.py"
|
|
)
|
|
|
|
env_site_packages.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy(sysconfigdata_path, env_site_packages)
|
|
host_site_packages = Path(get_hostsitepackages())
|
|
for name in get_unisolated_packages():
|
|
for path in chain(
|
|
host_site_packages.glob(f"{name}*"), host_site_packages.glob(f"_{name}*")
|
|
):
|
|
(env_site_packages / path.name).unlink(missing_ok=True)
|
|
(env_site_packages / path.name).symlink_to(path)
|
|
|
|
|
|
def remove_avoided_requirements(
|
|
requires: set[str], avoided_requirements: set[str] | list[str]
|
|
) -> set[str]:
|
|
for reqstr in list(requires):
|
|
req = Requirement(reqstr)
|
|
for avoid_name in set(avoided_requirements):
|
|
if avoid_name in req.name.lower():
|
|
requires.remove(reqstr)
|
|
return requires
|
|
|
|
|
|
def install_reqs(env: IsolatedEnv, reqs: set[str]) -> None:
|
|
env.install(
|
|
remove_avoided_requirements(
|
|
reqs, get_unisolated_packages() + AVOIDED_REQUIREMENTS
|
|
)
|
|
)
|
|
# Some packages (numcodecs) don't declare cython as a build dependency and
|
|
# only recythonize if it is present. We need them to always recythonize so
|
|
# we always install cython. If the reqs included some cython version already
|
|
# then this won't do anything.
|
|
env.install(
|
|
[
|
|
"cython",
|
|
"pythran",
|
|
"setuptools<65.6.0", # https://github.com/pypa/setuptools/issues/3693
|
|
]
|
|
)
|
|
|
|
|
|
def _build_in_isolated_env(
|
|
build_env: Mapping[str, str],
|
|
builder: ProjectBuilder,
|
|
outdir: str,
|
|
distribution: str,
|
|
config_settings: ConfigSettingsType,
|
|
) -> str:
|
|
# For debugging: The following line disables removal of the isolated venv.
|
|
# It will be left in the /tmp folder and can be inspected or entered as
|
|
# needed.
|
|
# _IsolatedEnvBuilder.__exit__ = lambda *args: None
|
|
with _IsolatedEnvBuilder() as env:
|
|
builder.python_executable = env.executable
|
|
builder.scripts_dir = env.scripts_dir
|
|
# first install the build dependencies
|
|
symlink_unisolated_packages(env)
|
|
install_reqs(env, builder.build_system_requires)
|
|
installed_requires_for_build = False
|
|
try:
|
|
build_reqs = builder.get_requires_for_build(
|
|
distribution,
|
|
)
|
|
except BuildBackendException:
|
|
pass
|
|
else:
|
|
install_reqs(env, build_reqs)
|
|
installed_requires_for_build = True
|
|
|
|
with replace_env(build_env):
|
|
if not installed_requires_for_build:
|
|
install_reqs(
|
|
env,
|
|
builder.get_requires_for_build(
|
|
distribution,
|
|
),
|
|
)
|
|
return builder.build(distribution, outdir, config_settings)
|
|
|
|
|
|
def parse_backend_flags(backend_flags: str) -> ConfigSettingsType:
|
|
config_settings: dict[str, str | list[str]] = {}
|
|
for arg in backend_flags.split():
|
|
setting, _, value = arg.partition("=")
|
|
if setting not in config_settings:
|
|
config_settings[setting] = value
|
|
continue
|
|
|
|
cur_value = config_settings[setting]
|
|
if isinstance(cur_value, str):
|
|
config_settings[setting] = [cur_value, value]
|
|
else:
|
|
cur_value.append(value)
|
|
return config_settings
|
|
|
|
|
|
def build(
|
|
build_env: Mapping[str, str], backend_flags: str, outdir: str | None = None
|
|
) -> str:
|
|
srcdir = Path.cwd()
|
|
if outdir is None:
|
|
outdir = str(srcdir / "dist")
|
|
builder = _ProjectBuilder(str(srcdir))
|
|
distribution = "wheel"
|
|
config_settings = parse_backend_flags(backend_flags)
|
|
try:
|
|
with _handle_build_error():
|
|
built = _build_in_isolated_env(
|
|
build_env, builder, outdir, distribution, config_settings
|
|
)
|
|
print("{bold}{green}Successfully built {}{reset}".format(built, **_STYLES))
|
|
return built
|
|
except Exception as e: # pragma: no cover
|
|
tb = traceback.format_exc().strip("\n")
|
|
print("\n{dim}{}{reset}\n".format(tb, **_STYLES))
|
|
_error(str(e))
|
|
sys.exit(1)
|