diff --git a/engraphis/cloud_license.py b/engraphis/cloud_license.py index f896ebe..2595826 100644 --- a/engraphis/cloud_license.py +++ b/engraphis/cloud_license.py @@ -29,6 +29,7 @@ import logging import math import os +import sys import tempfile import threading import urllib.error @@ -653,3 +654,34 @@ def revalidate(lic, key_material: str, *, base_url: Optional[str] = None) -> str return "offline" _write_lease(token) return "ok" + + +# ── compilation integrity guard — runs at import time ──────────────────────────── + + +def _verify_module_integrity(): + """Detect if this module was replaced with editable source after a compiled + extension was already installed (same check as licensing.py).""" + mod = sys.modules.get(__name__) + if mod is None: + return + f = getattr(mod, "__file__", "") + if not f: + return + if os.environ.get("ENGRAPHIS_DEV", "").strip() in ("1", "true", "yes"): + return + if not f.endswith(".py"): + return + from importlib.machinery import EXTENSION_SUFFIXES + dirname = os.path.dirname(f) + basename = os.path.splitext(os.path.basename(f))[0] + for suffix in EXTENSION_SUFFIXES: + if os.path.exists(os.path.join(dirname, basename + suffix)): + raise RuntimeError( + "Engraphis cloud_license integrity check failed: a compiled native " + "extension exists but is not being loaded. Reinstall from the " + "official distribution. For development, set ENGRAPHIS_DEV=1." + ) + + +_verify_module_integrity() diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..7bbbe88 --- /dev/null +++ b/setup.py @@ -0,0 +1,67 @@ +"""Build script — compiles licensing + cloud_license into native C extensions for +distribution. Dev installs (``pip install -e .``) skip Cython and use the plain .py +sources; ``python -m build`` / ``pip install .`` (release) compile them via Cython.""" + +import os + +from setuptools import Extension, setup +from setuptools.command.build_py import build_py as _build_py + + +SKIP_CYTHON = os.environ.get("ENGRAPHIS_SKIP_CYTHON", "").strip() == "1" +EXT_MODULES = [] + +if not SKIP_CYTHON: + try: + from Cython.Build import cythonize + except ImportError: + cythonize = None + + if cythonize is not None and not ( + # pip install -e . does NOT run build_ext, so skip. Detect via + # SETUPTOOLS_ENABLE_FEATURES (setuptools >= 69) or legacy editable flag. + os.environ.get("SETUPTOOLS_ENABLE_FEATURES", "") == "legacy-editable" + ): + EXT_MODULES = cythonize( + [ + Extension( + "engraphis.licensing", + ["engraphis/licensing.py"], + ), + Extension( + "engraphis.cloud_license", + ["engraphis/cloud_license.py"], + ), + ], + compiler_directives={ + "language_level": "3", + "boundscheck": False, + "wraparound": False, + }, + # Defer the dep files into the build dir so the engraphis/ source tree + # stays clean on every platform (Cython generates licensing.c etc.) + build_dir="build", + ) + + +class build_py(_build_py): + """Exclude licensing.py and cloud_license.py from the package when compiled + extensions exist — ship the .pyd/.so instead so the compiled version wins.""" + + def find_package_modules(self, package, package_dir): + modules = super().find_package_modules(package, package_dir) + if not EXT_MODULES: + return modules + return [ + m for m in modules + if (package, m[1]) not in { + ("engraphis", "licensing"), + ("engraphis", "cloud_license"), + } + ] + + +setup( + ext_modules=EXT_MODULES, + cmdclass={"build_py": build_py}, +)