|
1 |
| -# Copyright (c) 2018, 2021, Oracle and/or its affiliates. All rights reserved. |
| 1 | +# Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. |
2 | 2 | # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
3 | 3 | #
|
4 | 4 | # The Universal Permissive License (UPL), Version 1.0
|
|
36 | 36 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
37 | 37 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
38 | 38 | # SOFTWARE.
|
| 39 | +import sys |
| 40 | +import os |
| 41 | +import shutil |
| 42 | +import sysconfig |
39 | 43 |
|
40 |
| -# dummy file |
| 44 | +from importlib import invalidate_caches |
| 45 | +from pathlib import Path |
| 46 | + |
| 47 | + |
| 48 | +compiled_registry = set() |
| 49 | + |
| 50 | + |
| 51 | +def find_rootdir(): |
| 52 | + cur_dir = Path(__file__).parent |
| 53 | + while cur_dir.name != 'graalpython': |
| 54 | + cur_dir = cur_dir.parent |
| 55 | + rootdir = cur_dir.parent / "mxbuild" / "cpyexts" |
| 56 | + rootdir.mkdir(parents=True, exist_ok=True) |
| 57 | + return rootdir |
| 58 | + |
| 59 | + |
| 60 | +DIR = find_rootdir() |
| 61 | + |
| 62 | + |
| 63 | +def get_setuptools(setuptools='setuptools==67.6.1'): |
| 64 | + """ |
| 65 | + distutils is not part of std library since python 3.12 |
| 66 | + we rely on distutils to pick the toolchain for the underlying system |
| 67 | + and build the c extension tests. |
| 68 | + """ |
| 69 | + import site |
| 70 | + setuptools_path = find_rootdir() / ('%s-setuptools-venv' % sys.implementation.name) |
| 71 | + |
| 72 | + if not os.path.isdir(setuptools_path / 'setuptools'): |
| 73 | + import subprocess |
| 74 | + import venv |
| 75 | + print('installing setuptools in %s' % setuptools_path) |
| 76 | + venv.create(setuptools_path, with_pip=True) |
| 77 | + if sys.platform.startswith('win32'): |
| 78 | + py_executable = setuptools_path / 'Scripts' / 'python.exe' |
| 79 | + else: |
| 80 | + py_executable = setuptools_path / 'bin' / 'python3' |
| 81 | + extra_args = [] |
| 82 | + if sys.implementation.name == "graalpy" and __graalpython__.is_bytecode_dsl_interpreter: |
| 83 | + extra_args = ['--vm.Dpython.EnableBytecodeDSLInterpreter=true'] |
| 84 | + subprocess.run([py_executable, *extra_args, "-m", "pip", "install", "--target", str(setuptools_path), setuptools], check=True) |
| 85 | + print('setuptools is installed in %s' % setuptools_path) |
| 86 | + |
| 87 | + pyvenv_site = str(setuptools_path) |
| 88 | + if pyvenv_site not in site.getsitepackages(): |
| 89 | + site.addsitedir(pyvenv_site) |
| 90 | + |
| 91 | + |
| 92 | +def compile_module_from_string(c_source: str, name: str): |
| 93 | + source_file = DIR / f'{name}.c' |
| 94 | + with open(source_file, "wb", buffering=0) as f: |
| 95 | + f.write(bytes(c_source, 'utf-8')) |
| 96 | + return compile_module_from_file(name) |
| 97 | + |
| 98 | + |
| 99 | +def compile_module_from_file(module_name: str): |
| 100 | + install_dir = ccompile(None, module_name) |
| 101 | + sys.path.insert(0, install_dir) |
| 102 | + try: |
| 103 | + cmodule = __import__(module_name) |
| 104 | + finally: |
| 105 | + sys.path.pop(0) |
| 106 | + return cmodule |
| 107 | + |
| 108 | + |
| 109 | +def ccompile(self, name, check_duplicate_name=True): |
| 110 | + get_setuptools() |
| 111 | + from setuptools import setup, Extension |
| 112 | + from hashlib import sha256 |
| 113 | + EXT_SUFFIX = sysconfig.get_config_var("EXT_SUFFIX") |
| 114 | + |
| 115 | + source_file = DIR / f'{name}.c' |
| 116 | + file_not_empty(source_file) |
| 117 | + |
| 118 | + # compute checksum of source file |
| 119 | + m = sha256() |
| 120 | + with open(source_file,"rb") as f: |
| 121 | + # read 4K blocks |
| 122 | + for block in iter(lambda: f.read(4096),b""): |
| 123 | + m.update(block) |
| 124 | + cur_checksum = m.hexdigest() |
| 125 | + |
| 126 | + build_dir = DIR / 'build' / name |
| 127 | + |
| 128 | + # see if there is already a checksum file |
| 129 | + checksum_file = build_dir / f'{name}{EXT_SUFFIX}.sha256' |
| 130 | + available_checksum = "" |
| 131 | + if checksum_file.exists(): |
| 132 | + # read checksum file |
| 133 | + with open(checksum_file, "r") as f: |
| 134 | + available_checksum = f.readline() |
| 135 | + |
| 136 | + # note, the suffix is already a string like '.so' |
| 137 | + lib_file = build_dir / f'{name}{EXT_SUFFIX}' |
| 138 | + |
| 139 | + if check_duplicate_name and available_checksum != cur_checksum and name in compiled_registry: |
| 140 | + raise RuntimeError(f"\n\nModule with name '{name}' was already compiled, but with different source code. " |
| 141 | + "Have you accidentally used the same name for two different CPyExtType, CPyExtHeapType, " |
| 142 | + "or similar helper calls? Modules with same name can sometimes confuse the import machinery " |
| 143 | + "and cause all sorts of trouble.\n") |
| 144 | + |
| 145 | + compiled_registry.add(name) |
| 146 | + |
| 147 | + # Compare checksums and only re-compile if different. |
| 148 | + # Note: It could be that the C source file's checksum didn't change but someone |
| 149 | + # manually deleted the shared library file. |
| 150 | + if available_checksum != cur_checksum or not lib_file.exists(): |
| 151 | + os.makedirs(build_dir, exist_ok=True) |
| 152 | + # MSVC linker doesn't like absolute paths in some parameters, so just run from the build dir |
| 153 | + old_cwd = os.getcwd() |
| 154 | + os.chdir(build_dir) |
| 155 | + try: |
| 156 | + shutil.copy(source_file, '.') |
| 157 | + module = Extension(name, sources=[source_file.name]) |
| 158 | + args = [ |
| 159 | + '--verbose' if sys.flags.verbose else '--quiet', |
| 160 | + 'build', '--build-temp=t', '--build-base=b', '--build-purelib=l', '--build-platlib=l', |
| 161 | + 'install_lib', '-f', '--install-dir=.', |
| 162 | + ] |
| 163 | + setup( |
| 164 | + script_name='setup', |
| 165 | + script_args=args, |
| 166 | + name=name, |
| 167 | + version='1.0', |
| 168 | + description='', |
| 169 | + ext_modules=[module] |
| 170 | + ) |
| 171 | + finally: |
| 172 | + os.chdir(old_cwd) |
| 173 | + |
| 174 | + # write new checksum |
| 175 | + with open(checksum_file, "w") as f: |
| 176 | + f.write(cur_checksum) |
| 177 | + |
| 178 | + # IMPORTANT: |
| 179 | + # Invalidate caches after creating the native module. |
| 180 | + # FileFinder caches directory contents, and the check for directory |
| 181 | + # changes has whole-second precision, so it can miss quick updates. |
| 182 | + invalidate_caches() |
| 183 | + |
| 184 | + # ensure file was really written |
| 185 | + file_not_empty(lib_file) |
| 186 | + |
| 187 | + return str(build_dir) |
| 188 | + |
| 189 | + |
| 190 | +def file_not_empty(path): |
| 191 | + for i in range(3): |
| 192 | + try: |
| 193 | + stat_result = os.stat(path) |
| 194 | + if stat_result[6] != 0: |
| 195 | + return |
| 196 | + except FileNotFoundError: |
| 197 | + pass |
| 198 | + raise SystemError("file %s not available" % path) |
0 commit comments