mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-05 23:41:44 +00:00
Script for packing pyodide wheel
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
#
|
||||
# /// script
|
||||
# # Latest Pyodide build env versions are listed here:
|
||||
# # https://pyodide.github.io/pyodide/api/pyodide-cross-build-environments.json
|
||||
# # https://github.com/pyodide/pyodide-build/blob/main/pyodide_build/xbuildenv_releases.py
|
||||
# requires-python = "==3.13.2"
|
||||
# dependencies = [
|
||||
# "requests",
|
||||
# "setuptools",
|
||||
# ]
|
||||
# ///
|
||||
"""
|
||||
Pack an IfcOpenShell WASM wheel using Pyodide build system.
|
||||
|
||||
Usage:
|
||||
uv run make_wheel.py # Show this help
|
||||
uv run make_wheel.py --build # Build wheel
|
||||
uv run make_wheel.py --clean # Clean build artifacts and exit
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
|
||||
import requests
|
||||
|
||||
# Get repo root (parent of this script's parent directory)
|
||||
REPO_ROOT = Path(__file__).parent.parent
|
||||
PYODIDE_DIR = REPO_ROOT / "pyodide"
|
||||
BUILD_DIR = PYODIDE_DIR / "build"
|
||||
|
||||
# Hardcoded path (Windows packing workaround with --dev flag)
|
||||
PYODIDE_BUILD = Path(r"L:\Projects\Github\pyodide-build")
|
||||
|
||||
# Wheel platform tag (from PYODIDE_EMSCRIPTEN_VERSION in pyodide-build/Makefile.envs)
|
||||
WHEEL_PLATFORM_TAG = "emscripten_4_0_9_wasm32"
|
||||
|
||||
# Location where ifcopenshell will be extracted
|
||||
IFCOPENSHELL_DIR = PYODIDE_DIR / "ifcopenshell"
|
||||
|
||||
|
||||
class WheelBuilder:
|
||||
@staticmethod
|
||||
def extract_ifcopenshell_from_git(dst: Path) -> None:
|
||||
"""Extract ifcopenshell directory from git repo into destination."""
|
||||
Tools.rmrf(dst)
|
||||
|
||||
print(f"Extracting ifcopenshell from git to {dst}...")
|
||||
# Use git ls-files piped to git checkout-index to avoid copying
|
||||
# untracked or ignored files from the actual repo.
|
||||
ls_proc = subprocess.Popen(
|
||||
["git", "ls-files", "-z", "src/ifcopenshell-python/ifcopenshell"],
|
||||
cwd=REPO_ROOT,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
checkout_proc = subprocess.Popen(
|
||||
["git", "checkout-index", "-z", "--prefix", "pyodide/", "--stdin"],
|
||||
cwd=REPO_ROOT,
|
||||
stdin=ls_proc.stdout,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
assert ls_proc.stdout is not None
|
||||
ls_proc.stdout.close()
|
||||
checkout_proc.communicate()
|
||||
|
||||
if checkout_proc.returncode != 0:
|
||||
assert checkout_proc.stderr is not None
|
||||
raise RuntimeError(f"Failed to extract: {checkout_proc.stderr.decode()}")
|
||||
|
||||
# Move src/ifcopenshell-python/ifcopenshell to ifcopenshell.
|
||||
temp_src = PYODIDE_DIR / "src" / "ifcopenshell-python" / "ifcopenshell"
|
||||
shutil.move(temp_src, dst)
|
||||
|
||||
# Clean up temporary src directory.
|
||||
Tools.rmrf(PYODIDE_DIR / "src")
|
||||
|
||||
print("✓ Extracted ifcopenshell from git")
|
||||
|
||||
@staticmethod
|
||||
def get_wheel_url(makefile_path: Path) -> str:
|
||||
"""Get S3 wheel URL based on BINARY_VERSION and BUILD_COMMIT from Makefile."""
|
||||
|
||||
def parse_makefile_vars() -> dict[str, str]:
|
||||
content = makefile_path.read_text()
|
||||
vars: dict[str, str] = {}
|
||||
for match in re.finditer(r"^(BINARY_VERSION|BUILD_COMMIT):=(.+)$", content, re.MULTILINE):
|
||||
vars[match.group(1)] = match.group(2).strip()
|
||||
return vars
|
||||
|
||||
vars: dict[str, str] = parse_makefile_vars()
|
||||
binary_version = vars["BINARY_VERSION"]
|
||||
build_commit = vars["BUILD_COMMIT"]
|
||||
filename = f"ifcopenshell-{binary_version}+{build_commit}-cp313-cp313-pyodide_2025_0_wasm32.whl"
|
||||
encoded_filename = quote(filename, safe="")
|
||||
return f"https://s3.amazonaws.com/ifcopenshell-builds/{encoded_filename}"
|
||||
|
||||
@staticmethod
|
||||
def download_and_extract_so(url: str, build_dir: Path) -> tuple[Path, Path]:
|
||||
"""Download wheel from URL and extract .so and .py files."""
|
||||
py_wrapper_filename = "ifcopenshell_wrapper.py"
|
||||
build_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
wheel_path = build_dir / url.rsplit("/", 1)[-1]
|
||||
|
||||
if wheel_path.exists():
|
||||
print(f"Using cached wheel: {wheel_path}")
|
||||
else:
|
||||
print(f"Downloading {url}...")
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
wheel_path.write_bytes(response.content)
|
||||
|
||||
print("Extracting _ifcopenshell_wrapper files...")
|
||||
with zipfile.ZipFile(wheel_path) as zf:
|
||||
so_files = [f for f in zf.namelist() if f.endswith(".so")]
|
||||
py_files = [f for f in zf.namelist() if f.endswith(py_wrapper_filename)]
|
||||
|
||||
assert so_files, "No .so file found in wheel"
|
||||
assert py_files, f"No {py_wrapper_filename} file found in wheel"
|
||||
|
||||
so_file = so_files[0]
|
||||
so_dst = build_dir / Path(so_file).name
|
||||
so_dst.write_bytes(zf.read(so_file))
|
||||
|
||||
py_file = py_files[0]
|
||||
py_dst = build_dir / Path(py_file).name
|
||||
py_dst.write_bytes(zf.read(py_file))
|
||||
|
||||
return so_dst, py_dst
|
||||
|
||||
|
||||
class Tools:
|
||||
@staticmethod
|
||||
def run(
|
||||
cmd: list[str],
|
||||
cwd: Path | None = None,
|
||||
) -> None:
|
||||
print(f"$ {' '.join(cmd)}")
|
||||
subprocess.check_call(cmd, cwd=cwd)
|
||||
|
||||
@staticmethod
|
||||
def create_symlink(dst: Path, src: Path) -> None:
|
||||
Tools.rmrf(dst)
|
||||
dst.symlink_to(src)
|
||||
|
||||
@staticmethod
|
||||
def rmrf(path: Path) -> None:
|
||||
if path.exists() or path.is_symlink():
|
||||
if path.is_dir() and not path.is_symlink():
|
||||
shutil.rmtree(path)
|
||||
else:
|
||||
path.unlink()
|
||||
|
||||
|
||||
def clean() -> None:
|
||||
"""Remove build artifacts."""
|
||||
paths_to_remove = (
|
||||
BUILD_DIR,
|
||||
PYODIDE_DIR / ".pyodide_build",
|
||||
PYODIDE_DIR / "dist",
|
||||
PYODIDE_DIR / "ifcopenshell.egg-info",
|
||||
PYODIDE_DIR / "src",
|
||||
IFCOPENSHELL_DIR,
|
||||
)
|
||||
for path in paths_to_remove:
|
||||
if path.exists() or path.is_symlink():
|
||||
print(f"Removing {path}...")
|
||||
Tools.rmrf(path)
|
||||
print("✓ Clean complete")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__, add_help=False)
|
||||
parser.add_argument("--build", action="store_true", help="Build the wheel")
|
||||
parser.add_argument("--clean", action="store_true", help="Clean build folder")
|
||||
parser.add_argument(
|
||||
"--dev",
|
||||
action="store_true",
|
||||
help="Use editable pyodide-build from hardcoded path (Windows packing workaround)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.build and not args.clean:
|
||||
print(__doc__)
|
||||
return
|
||||
|
||||
if args.clean:
|
||||
clean()
|
||||
return
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
WheelBuilder.extract_ifcopenshell_from_git(IFCOPENSHELL_DIR)
|
||||
|
||||
print("Downloading and extracting _ifcopenshell_wrapper files...")
|
||||
makefile = REPO_ROOT / "src" / "ifcopenshell-python" / "Makefile"
|
||||
wheel_url = WheelBuilder.get_wheel_url(makefile)
|
||||
so_file, py_file = WheelBuilder.download_and_extract_so(wheel_url, BUILD_DIR)
|
||||
|
||||
Tools.create_symlink(IFCOPENSHELL_DIR / Path(so_file).name, so_file)
|
||||
Tools.create_symlink(IFCOPENSHELL_DIR / Path(py_file).name, py_file)
|
||||
|
||||
print("Installing pyodide-build...")
|
||||
if args.dev:
|
||||
Tools.run(["uv", "pip", "install", "-e", str(PYODIDE_BUILD)])
|
||||
else:
|
||||
Tools.run(["uv", "pip", "install", "pyodide-build"])
|
||||
|
||||
print("Building with pyodide...")
|
||||
# Use --no-isolation due to pyodide-build Windows support issues:
|
||||
# symlink_unisolated_packages fails with missing `_sysconfigdata_$(CPYTHON_ABI_FLAGS)_emscripten_wasm32-emscripten.py`.
|
||||
# Hardcode platform name since pyodide doesn't yet support overriding wheel tags on Windows.
|
||||
#
|
||||
# Use `LEGACY_PLATFORM` since pyodide 0.34.1 introduced new tag for wheels `pyemscripten`,
|
||||
# which doesn't work with pyodide itself yet - https://github.com/pyodide/pyodide/issues/6177.
|
||||
os.environ["USE_LEGACY_PLATFORM"] = "1"
|
||||
Tools.run(["pyodide", "build", f"-C--build-option=--plat-name={WHEEL_PLATFORM_TAG}"])
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
print(f"\n✓ Done! ({elapsed:.1f}s)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+39
-1
@@ -2,12 +2,16 @@
|
||||
# because `tool.setuptools.ext-modules` is still experimental in pyproject.toml
|
||||
# and we need it to get the wheel suffix right.
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import tomllib
|
||||
from setuptools import Extension, find_packages, setup
|
||||
from setuptools.command.build_ext import build_ext
|
||||
|
||||
REPO_FOLDER = Path(__file__).parent
|
||||
# Detect repo folder: if setup.py is in pyodide folder, go to parent
|
||||
SETUP_DIR = Path(__file__).parent
|
||||
REPO_FOLDER = SETUP_DIR.parent if SETUP_DIR.name == "pyodide" else SETUP_DIR
|
||||
|
||||
|
||||
def get_version() -> str:
|
||||
@@ -25,6 +29,39 @@ def get_dependencies() -> list[str]:
|
||||
return dependencies
|
||||
|
||||
|
||||
class UnixBuildExt(build_ext):
|
||||
"""Customize ``build_ext`` to support packing on Windows."""
|
||||
|
||||
def finalize_options(self):
|
||||
from distutils import sysconfig
|
||||
|
||||
super().finalize_options()
|
||||
if sys.platform == "win32":
|
||||
self.compiler = "unix"
|
||||
|
||||
# Configure sysconfig for Windows builds
|
||||
# CCSHARED is the only variable that's not customizable with env vars.
|
||||
# Basically avoiding this:
|
||||
# File ".venv\Lib\site-packages\setuptools\_distutils\sysconfig.py", line 366, in customize_compiler
|
||||
# compiler_so=cc_cmd + ' ' + ccshared,
|
||||
# ~~~~~~~~~~~~~^~~~~~~~~~
|
||||
# TypeError: can only concatenate str (not "NoneType") to str
|
||||
sysconfig.get_config_vars() # Initialize config cache
|
||||
if sysconfig._config_vars.get("CCSHARED") is None:
|
||||
sysconfig._config_vars["CCSHARED"] = "-fPIC"
|
||||
# Override compiler type before it's instantiated
|
||||
|
||||
# Set Emscripten compiler environment variables
|
||||
os.environ["CC"] = "emcc"
|
||||
os.environ["CXX"] = "em++"
|
||||
os.environ["CFLAGS"] = ""
|
||||
os.environ["CXXFLAGS"] = ""
|
||||
os.environ["LDSHARED"] = "emcc -shared"
|
||||
os.environ["AR"] = "emar"
|
||||
os.environ["ARFLAGS"] = "rcs"
|
||||
os.environ["SETUPTOOLS_EXT_SUFFIX"] = ".cpython-313-wasm32-emscripten.so"
|
||||
|
||||
|
||||
setup(
|
||||
name="ifcopenshell",
|
||||
version=get_version(),
|
||||
@@ -44,4 +81,5 @@ setup(
|
||||
},
|
||||
# Has to provide extension to get the correct wheel suffix.
|
||||
ext_modules=[Extension("ifcopenshell._ifcopenshell_wrapper", sources=[])],
|
||||
cmdclass={"build_ext": UnixBuildExt},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user