Skip to content

Commit 5aa136d

Browse files
committed
Move code to compile C extensions from some code to the tests module and reuse in cpyext tests and C extension micro-benchmark harness
1 parent 68be29c commit 5aa136d

File tree

3 files changed

+177
-179
lines changed

3 files changed

+177
-179
lines changed

graalpython/com.oracle.graal.python.benchmarks/python/harness.py

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -205,27 +205,19 @@ def warmup(cp_index):
205205

206206

207207
def ccompile(name, code):
208-
from importlib import invalidate_caches
209-
from distutils.core import setup, Extension
210-
__dir__ = __file__.rpartition("/")[0]
211-
source_file = '%s/%s.c' % (__dir__, name)
212-
with open(source_file, "w") as f:
213-
f.write(code)
214-
module = Extension(name, sources=[source_file])
215-
args = ['--quiet', 'build', 'install_lib', '-f', '--install-dir=%s' % __dir__]
216-
setup(
217-
script_name='setup',
218-
script_args=args,
219-
name=name,
220-
version='1.0',
221-
description='',
222-
ext_modules=[module]
223-
)
224-
# IMPORTANT:
225-
# Invalidate caches after creating the native module.
226-
# FileFinder caches directory contents, and the check for directory
227-
# changes has whole-second precision, so it can miss quick updates.
228-
invalidate_caches()
208+
import sys, os
209+
210+
rootdir = os.path.dirname(__file__)
211+
while os.path.basename(rootdir) != 'graalpython':
212+
rootdir = os.path.dirname(rootdir)
213+
214+
sys.path.append(os.path.join(
215+
rootdir,
216+
"com.oracle.graal.python.test",
217+
"src",
218+
))
219+
from tests import compile_module_from_string
220+
compile_module_from_string(code, name)
229221

230222

231223
def _as_int(value):

graalpython/com.oracle.graal.python.test/src/tests/__init__.py

Lines changed: 160 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
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.
22
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
33
#
44
# The Universal Permissive License (UPL), Version 1.0
@@ -36,5 +36,163 @@
3636
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
3737
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
3838
# SOFTWARE.
39+
import sys
40+
import os
41+
import shutil
42+
import sysconfig
3943

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

Comments
 (0)