diff --git a/server/pypi/build-wheel.py b/server/pypi/build-wheel.py index f7e33ac8f7..dd758113b5 100755 --- a/server/pypi/build-wheel.py +++ b/server/pypi/build-wheel.py @@ -17,6 +17,7 @@ import tempfile from textwrap import dedent +import build from elftools.elf.elffile import ELFFile import jinja2 import jsonschema @@ -116,6 +117,11 @@ def unpack_and_build(self): self.python_tag = self.non_python_tag self.compat_tag = f"{self.python_tag}-android_{self.api_level}_{self.abi_tag}" + # TODO: move this to {PYPI_DIR}/build/{package}/{version}, which is one level + # shallower, more consistent with the layout of dist/ and packages/, and keeps + # all the build directories together for easier cleanup. But first, check + # whether any build scripts or patches use relative paths to get things from the + # RECIPE_DIR, and make them use the environment variable instead. self.version_dir = f"{self.package_dir}/build/{self.version}" ensure_dir(self.version_dir) cd(self.version_dir) @@ -123,8 +129,8 @@ def unpack_and_build(self): self.src_dir = f"{self.build_dir}/src" if self.no_unpack: - log("Skipping download and unpack due to --no-unpack") - assert_isdir(self.build_dir) + log("Reusing existing build directory due to --no-unpack") + assert_isdir(self.src_dir) else: ensure_empty(self.build_dir) self.unpack_source() @@ -132,9 +138,10 @@ def unpack_and_build(self): self.build_env = f"{self.build_dir}/env" self.host_env = f"{self.build_dir}/requirements" - if self.no_reqs: - log("Skipping requirements due to --no-reqs") - else: + self.builder = build.ProjectBuilder( + self.src_dir, python_executable=f"{self.build_env}/bin/python") + + if not self.no_unpack: os.environ["PIP_DISABLE_PIP_VERSION_CHECK"] = "1" if self.needs_python: self.create_build_env() @@ -154,13 +161,11 @@ def parse_args(self): ap.add_argument("-v", "--verbose", action="store_true", help="Log more detail") skip_group = ap.add_mutually_exclusive_group() - skip_group.add_argument("--no-unpack", action="store_true", help="Skip download and unpack " - "(an existing build subdirectory must exist, and will be reused)") - skip_group.add_argument("--no-build", action="store_true", help="Download and unpack, but " - "skip build") + skip_group.add_argument("--no-unpack", action="store_true", + help="Reuse an existing build directory") + skip_group.add_argument("--no-build", action="store_true", + help="Prepare the build directory, but skip the build") - ap.add_argument("--no-reqs", action="store_true", help="Skip installing requirements " - "(existing environments will be reused)") ap.add_argument("--abi", metavar="ABI", required=True, choices=ABIS, help="Android ABI: choices=[%(choices)s]") ap.add_argument("--api-level", metavar="LEVEL", type=int, default=21, @@ -205,20 +210,31 @@ def find_target(self): self.target_zip = zips[0] def create_build_env(self): - # Installing Python's bundled pip and setuptools into the environment is - # pointless since we'd immediately have to replace them anyway. Instead, we - # create one bootstrap environment per Python version, shared between all - # packages, and use that to install the build environments. This saves about 3.5 - # seconds per build on Python 3.8, and 6 seconds on Python 3.11. + # Installing Python's bundled pip and setuptools into a new environment takes + # about 3.5 seconds on Python 3.8, and 6 seconds on Python 3.11. To avoid this, + # we create one bootstrap environment per Python version, shared between all + # packages, and use that to install the build environments. bootstrap_env = self.get_bootstrap_env() ensure_empty(self.build_env) run(f"python{self.python} -m venv --without-pip {self.build_env}") - build_reqs = {"pip": "19.3", "setuptools": "67.0.0", "wheel": "0.33.6"} - build_reqs.update(self.get_requirements("build")) - run(f"{bootstrap_env}/bin/pip --python {self.build_env}/bin/python install " + - ("-v " if self.verbose else "") + - (" ".join(f"{name}=={version}" for name, version in build_reqs.items()))) + # In case meta.yaml and pyproject.toml have requirements for the same package, + # listing the more specific requirements first will help pip find a solution + # faster. + build_reqs = ([f"{package}=={version}" + for package, version in self.get_requirements("build")] + + list(self.builder.build_system_requires)) + + def pip_install(requirements): + if not requirements: + return + run(f"{bootstrap_env}/bin/pip --python {self.builder.python_executable} " + f"install " + " ".join(shlex.quote(req) for req in requirements)) + + # In the common case where get_requires_for_build only returns "wheel", which + # was already in build_system_requires, we can avoid running pip a second time. + pip_install(build_reqs) + pip_install(self.builder.get_requires_for_build("wheel") - set(build_reqs)) def get_bootstrap_env(self): bootstrap_env = f"{PYPI_DIR}/build/_bootstrap/{self.python}" @@ -268,12 +284,6 @@ def unpack_source(self): else: run(f"mv {temp_dir} {self.src_dir}") - # pyproject.toml may conflict with our own requirements mechanism, so we currently - # disable it. - if exists(f"{self.src_dir}/pyproject.toml"): - run(f"mv {self.src_dir}/pyproject.toml " - f"{self.src_dir}/pyproject-chaquopy-disabled.toml") - def download_git(self, source): git_rev = source["git_rev"] is_hash = len(str(git_rev)) == 40 @@ -355,7 +365,7 @@ def build_wheel(self): if exists(build_script): return self.build_with_script(build_script) elif self.needs_python: - return self.build_with_pip() + return self.build_with_pep517() else: raise CommandError("Don't know how to build: no build.sh exists, and this is not " "declared as a Python package. Do you need to add a `host` " @@ -439,18 +449,8 @@ def build_with_script(self, build_script): run(build_script) return self.package_wheel(prefix_dir, self.src_dir) - def build_with_pip(self): - # We can't run "setup.py bdist_wheel" directly, because that would only work with - # setuptools-aware setup.py files. - run(f"{self.build_env}/bin/pip wheel --no-deps " - # --no-clean doesn't currently work: see env/python/sitecustomize.py. - # - # We pass -v unconditionally, because we always want to see the build process - # output. --global-option=-vv enables additional distutils logging. - f"-v {'--global-option=-vv ' if self.verbose else ''}" - f"-e .") - wheel_filename, = glob("*.whl") # Note comma - return abspath(wheel_filename) + def build_with_pep517(self): + return self.builder.build("wheel", "dist") def update_env(self): env = {} diff --git a/server/pypi/packages/numpy/meta.yaml b/server/pypi/packages/numpy/meta.yaml index 4a6f08442b..36af2d2e1f 100644 --- a/server/pypi/packages/numpy/meta.yaml +++ b/server/pypi/packages/numpy/meta.yaml @@ -8,6 +8,5 @@ build: requirements: build: - Cython 0.29.32 - - setuptools 63.0.0 # Issue with setuptools >= 64 cf https://github.com/pypa/setuptools/issues/3549 host: - chaquopy-openblas 0.2.20 diff --git a/server/pypi/packages/numpy/patches/chaquopy.patch b/server/pypi/packages/numpy/patches/chaquopy.patch index c4428d4ed7..4a3d2e20c4 100644 --- a/server/pypi/packages/numpy/patches/chaquopy.patch +++ b/server/pypi/packages/numpy/patches/chaquopy.patch @@ -151,3 +151,17 @@ diff -ur src-original/setup.py src/setup.py CLASSIFIERS = """\ Development Status :: 5 - Production/Stable Intended Audience :: Science/Research +--- src-original/pyproject.toml 2022-09-09 13:36:30.619459000 +0000 ++++ src/pyproject.toml 2023-08-31 11:38:08.304445745 +0000 +@@ -2,7 +2,10 @@ + # Minimum requirements for the build system to execute. + requires = [ + "packaging==20.5; platform_machine=='arm64'", # macos M1 +- "setuptools==59.2.0", ++ ++ # Chaquopy: was 59.2.0, which failed to detect modf and frexp for some reason. ++ "setuptools==63.0.0", ++ + "wheel==0.37.0", + "Cython>=0.29.30,<3.0", + ] diff --git a/server/pypi/requirements.txt b/server/pypi/requirements.txt index 1500b66855..8f1709887d 100644 --- a/server/pypi/requirements.txt +++ b/server/pypi/requirements.txt @@ -1,8 +1,5 @@ -# These are the requirements of build-wheel itself. -# -# Requirements of the build environment are within the script at `create_build_env`. - # Direct requirements +build==0.10.0 Jinja2==2.11.3 jsonschema==2.6.0 pyelftools==0.29 @@ -21,7 +18,9 @@ mailbits==0.2.1 MarkupSafe==2.0.1 packaging==23.1 pydantic==1.10.12 +pyproject_hooks==1.0.0 requests==2.31.0 soupsieve==2.4.1 +tomli==2.0.1; python_version < "3.11" typing-extensions==4.7.1 urllib3==2.0.4