Skip to content
Open
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
7 changes: 7 additions & 0 deletions interpreters/python/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ config INTERPRETERS_CPYTHON_ENABLE_PIP
site-packages .pth entry that points to ensurepip's bundled wheel.
Disable this to skip pip wheel download/integration entirely.

config INTERPRETERS_CPYTHON_INSTALL_NUTTX_PACKAGE
bool "Install NuttX Python package"
default y
---help---
Installs the nuttx-periphery Python package into the CPython module image.
This allows importing the nuttx-periphery package into the CPython interpreter,
which provides a Python interface to the NuttX's peripheral drivers.

config INTERPRETERS_CPYTHON_PYTHONPATH
string "CPython Python path"
Expand Down
30 changes: 25 additions & 5 deletions interpreters/python/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -179,25 +179,45 @@ $(TARGETBUILD)/Makefile: $(HOSTPYTHON) $(CONFIG_SITE) $(SETUP_LOCAL)
$(Q) sed -i 's/^#define HAVE_LIBB2 1/\/* #undef HAVE_LIBB2 *\//' $(TARGETBUILD)/pyconfig.h
$(Q) sed -i 's/-lb2//g' $(TARGETBUILD)/Makefile

BUNDLED_WHEELS_DIR = $(CPYTHON_PATH)/Lib/ensurepip/_bundled
# PyPI downloads use the host system pip, not $(HOSTPYTHON)'s ensurepip bundle:
# pip 24.x vendors urllib3 that breaks on Python 3.13 (zlib.Decompress.unused_data).
HOSTPIP = python3 -m pip

$(TARGETLIBPYTHON): $(TARGETBUILD)/Makefile
ifneq ($(or $(filter y,$(CONFIG_INTERPRETERS_CPYTHON_ENABLE_PIP)),$(filter y,$(CONFIG_INTERPRETERS_CPYTHON_INSTALL_NUTTX_PACKAGE))),)
$(Q) mkdir -p $(BUNDLED_WHEELS_DIR)
endif
ifeq ($(CONFIG_INTERPRETERS_CPYTHON_ENABLE_PIP),y)
$(Q) mkdir -p $(CPYTHON_PATH)/Lib/ensurepip/_bundled
$(Q) ( \
PIP_WHEEL_VERSION=$$($(HOSTPYTHON) -c "import ensurepip; print(ensurepip._PIP_VERSION)"); \
PIP_WHEEL=$(CPYTHON_PATH)/Lib/ensurepip/_bundled/pip-$${PIP_WHEEL_VERSION}-py3-none-any.whl; \
PIP_WHEEL=$(BUNDLED_WHEELS_DIR)/pip-$${PIP_WHEEL_VERSION}-py3-none-any.whl; \
if [ ! -f "$${PIP_WHEEL}" ]; then \
echo "Fetching pip wheel $${PIP_WHEEL_VERSION} for ensurepip bundle"; \
$(HOSTPYTHON) -m pip download --only-binary=:all: --no-deps --dest $(CPYTHON_PATH)/Lib/ensurepip/_bundled pip==$${PIP_WHEEL_VERSION}; \
$(HOSTPIP) download --only-binary=:all: --no-deps --dest $(BUNDLED_WHEELS_DIR) pip==$${PIP_WHEEL_VERSION}; \
fi; \
echo "Pre-compiling pip wheel with build Python (must match embedded CPython version)"; \
$(HOSTPYTHON) $(CURDIR)/repack_wheel_add_pyc.py "$${PIP_WHEEL}"; \
$(HOSTPYTHON) $(CURDIR)/repack_wheel_add_pyc.py --package pip "$${PIP_WHEEL}"; \
)
endif
ifeq ($(CONFIG_INTERPRETERS_CPYTHON_INSTALL_NUTTX_PACKAGE),y)
$(Q) ( \
set -e; \
NUTTX_WHEEL=$$(ls $(BUNDLED_WHEELS_DIR)/nuttx_periphery-*-py3-none-any.whl 2>/dev/null | head -1 || true); \
if [ -z "$${NUTTX_WHEEL}" ]; then \
echo "Fetching latest nuttx-periphery wheel for ensurepip bundle"; \
$(HOSTPIP) download --only-binary=:all: --no-deps --dest $(BUNDLED_WHEELS_DIR) nuttx-periphery; \
NUTTX_WHEEL=$$(ls $(BUNDLED_WHEELS_DIR)/nuttx_periphery-*-py3-none-any.whl); \
fi; \
echo "Pre-compiling nuttx-periphery wheel with build Python (must match embedded CPython version)"; \
$(HOSTPYTHON) $(CURDIR)/repack_wheel_add_pyc.py --package nuttx_periphery "$${NUTTX_WHEEL}"; \
)
endif
$(MAKE) -C $(TARGETBUILD) regen-frozen
$(MAKE) -C $(TARGETBUILD) libpython$(CPYTHON_VERSION_MINOR).a wasm_stdlib
$(Q) ( cp $(TARGETBUILD)/libpython$(CPYTHON_VERSION_MINOR).a $(TARGETLIBPYTHON) )
$(Q) $(UNPACK) $(TARGETMODULESPACK) -d $(TARGETMODULES)/python$(CPYTHON_VERSION_MINOR)
ifeq ($(CONFIG_INTERPRETERS_CPYTHON_ENABLE_PIP),y)
ifneq ($(or $(filter y,$(CONFIG_INTERPRETERS_CPYTHON_ENABLE_PIP)),$(filter y,$(CONFIG_INTERPRETERS_CPYTHON_INSTALL_NUTTX_PACKAGE))),)
$(Q) mkdir -p $(TARGETMODULES)/python$(CPYTHON_VERSION_MINOR)/site-packages
$(Q) ( \
set -e; \
Expand Down
65 changes: 38 additions & 27 deletions interpreters/python/repack_wheel_add_pyc.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
#
# Repack a wheel in-place: compile pip/*.py to legacy sibling *.pyc (compileall
# -b: required for zipimport, which does not read PEP 3147 __pycache__/ names),
# remove the .py sources, and rewrite *.dist-info/RECORD.
# Repack a wheel in-place: compile package/*.py to legacy sibling *.pyc
# (compileall -b: required for zipimport, which does not read PEP 3147
# __pycache__/ names), remove the .py sources, and rewrite *.dist-info/RECORD.

from __future__ import annotations

Expand All @@ -23,25 +23,27 @@ def wheel_record_hash(data: bytes) -> str:
return base64.urlsafe_b64encode(digest).decode("ascii").rstrip("=")


def wheel_has_pip_py_sources(zf: zipfile.ZipFile) -> bool:
return any(n.startswith("pip/") and n.endswith(".py") for n in zf.namelist())
def wheel_has_py_sources(zf: zipfile.ZipFile, package: str) -> bool:
prefix = f"{package}/"
return any(n.startswith(prefix) and n.endswith(".py") for n in zf.namelist())


def wheel_has_legacy_pip_bytecode(zf: zipfile.ZipFile) -> bool:
return "pip/__init__.pyc" in zf.namelist()
def wheel_has_legacy_bytecode(zf: zipfile.ZipFile, package: str) -> bool:
return f"{package}/__init__.pyc" in zf.namelist()


def strip_pip_py_sources(pip_dir: Path) -> int:
"""Remove pip/**/*.py after sibling legacy *.pyc exists (compileall -b output)."""
def strip_py_sources(pkg_dir: Path, package: str) -> int:
"""Remove package/**/*.py after sibling legacy *.pyc exists (compileall -b output)."""
removed = 0
for path in sorted(pip_dir.rglob("*.py")):
for path in sorted(pkg_dir.rglob("*.py")):
if not path.is_file():
continue
pyc = path.with_suffix(".pyc")
if not pyc.is_file():
rel = path.relative_to(pip_dir)
rel = path.relative_to(pkg_dir)
raise SystemExit(
f"missing legacy .pyc for pip/{rel.as_posix()}, refusing to delete source"
f"missing legacy .pyc for {package}/{rel.as_posix()}, "
"refusing to delete source"
)
path.unlink()
removed += 1
Expand Down Expand Up @@ -70,41 +72,43 @@ def rebuild_record(root: Path) -> None:
record_path.write_text("\n".join(lines) + "\n", encoding="utf-8")


def repack(whl_path: Path, *, force: bool) -> None:
def repack(whl_path: Path, *, package: str, force: bool) -> None:
whl_path = whl_path.resolve()
if not whl_path.is_file():
raise SystemExit(f"missing wheel: {whl_path}")

with zipfile.ZipFile(whl_path) as zf:
has_py = wheel_has_pip_py_sources(zf)
has_py = wheel_has_py_sources(zf, package)
if not has_py:
if not wheel_has_legacy_pip_bytecode(zf):
if not wheel_has_legacy_bytecode(zf, package):
raise SystemExit(
"repack_wheel_add_pyc: wheel has no pip/*.py and no pip/__init__.pyc "
"(corrupt or old tool output). Delete the bundled pip-*.whl and rebuild."
f"repack_wheel_add_pyc: wheel has no {package}/*.py and no "
f"{package}/__init__.pyc (corrupt or old tool output). "
f"Delete the bundled wheel and rebuild."
)
if not force:
print(
f"repack_wheel_add_pyc: skip (pip already bytecode-only): {whl_path.name}"
f"repack_wheel_add_pyc: skip ({package} already bytecode-only): "
f"{whl_path.name}"
)
return

tmpdir = tempfile.mkdtemp(prefix="pip-whl-pyc-")
tmpdir = tempfile.mkdtemp(prefix=f"{package}-whl-pyc-")
try:
root = Path(tmpdir)
with zipfile.ZipFile(whl_path) as zf:
zf.extractall(root)

pip_dir = root / "pip"
if not pip_dir.is_dir():
raise SystemExit("wheel has no pip/ top-level package")
pkg_dir = root / package
if not pkg_dir.is_dir():
raise SystemExit(f"wheel has no {package}/ top-level package")

subprocess.run(
[sys.executable, "-m", "compileall", "-q", "-f", "-b", str(pip_dir)],
[sys.executable, "-m", "compileall", "-q", "-f", "-b", str(pkg_dir)],
cwd=str(root),
check=True,
)
n_py = strip_pip_py_sources(pip_dir)
n_py = strip_py_sources(pkg_dir, package)
rebuild_record(root)

out_path = whl_path.with_suffix(whl_path.suffix + ".tmp")
Expand All @@ -116,7 +120,8 @@ def repack(whl_path: Path, *, force: bool) -> None:

out_path.replace(whl_path)
print(
f"repack_wheel_add_pyc: bytecode-only pip ({n_py} .py removed) -> {whl_path.name}"
f"repack_wheel_add_pyc: bytecode-only {package} ({n_py} .py removed) -> "
f"{whl_path.name}"
)
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
Expand All @@ -125,14 +130,20 @@ def repack(whl_path: Path, *, force: bool) -> None:
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("wheel", type=Path, help="path to .whl (updated in place)")
ap.add_argument(
"-p",
"--package",
default="pip",
help="top-level package directory inside the wheel (default: pip)",
)
ap.add_argument(
"-f",
"--force",
action="store_true",
help="repack even if pip is already .pyc-only",
help="repack even if the package is already .pyc-only",
)
args = ap.parse_args()
repack(args.wheel, force=args.force)
repack(args.wheel, package=args.package, force=args.force)


if __name__ == "__main__":
Expand Down
Loading