Skip to content

Commit e5bce5c

Browse files
committed
[GR-65683] Compile C extensions with setuptools in a venv for our cpyext tests and micro-benchmarks.
PullRequest: graalpython/3831
2 parents 88f7c58 + 2f7218c commit e5bce5c

File tree

6 files changed

+190
-187
lines changed

6 files changed

+190
-187
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: 165 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,168 @@
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, sibling_to=None):
100+
if sibling_to:
101+
compile_name = str(Path(sibling_to).absolute().parent / module_name)
102+
else:
103+
compile_name = module_name
104+
install_dir = ccompile(None, compile_name)
105+
sys.path.insert(0, install_dir)
106+
try:
107+
cmodule = __import__(module_name)
108+
finally:
109+
sys.path.pop(0)
110+
return cmodule
111+
112+
113+
def ccompile(self, name, check_duplicate_name=True):
114+
get_setuptools()
115+
from setuptools import setup, Extension
116+
from hashlib import sha256
117+
EXT_SUFFIX = sysconfig.get_config_var("EXT_SUFFIX")
118+
119+
source_file = DIR / f'{name}.c'
120+
filename = Path(name).name
121+
file_not_empty(source_file)
122+
123+
# compute checksum of source file
124+
m = sha256()
125+
with open(source_file,"rb") as f:
126+
# read 4K blocks
127+
for block in iter(lambda: f.read(4096),b""):
128+
m.update(block)
129+
cur_checksum = m.hexdigest()
130+
131+
build_dir = DIR / 'build' / filename
132+
133+
# see if there is already a checksum file
134+
checksum_file = build_dir / f'{filename}{EXT_SUFFIX}.sha256'
135+
available_checksum = ""
136+
if checksum_file.exists():
137+
# read checksum file
138+
with open(checksum_file, "r") as f:
139+
available_checksum = f.readline()
140+
141+
# note, the suffix is already a string like '.so'
142+
lib_file = build_dir / f'{filename}{EXT_SUFFIX}'
143+
144+
if check_duplicate_name and available_checksum != cur_checksum and name in compiled_registry:
145+
raise RuntimeError(f"\n\nModule with name '{name}' was already compiled, but with different source code. "
146+
"Have you accidentally used the same name for two different CPyExtType, CPyExtHeapType, "
147+
"or similar helper calls? Modules with same name can sometimes confuse the import machinery "
148+
"and cause all sorts of trouble.\n")
149+
150+
compiled_registry.add(name)
151+
152+
# Compare checksums and only re-compile if different.
153+
# Note: It could be that the C source file's checksum didn't change but someone
154+
# manually deleted the shared library file.
155+
if available_checksum != cur_checksum or not lib_file.exists():
156+
os.makedirs(build_dir, exist_ok=True)
157+
# MSVC linker doesn't like absolute paths in some parameters, so just run from the build dir
158+
old_cwd = os.getcwd()
159+
os.chdir(build_dir)
160+
try:
161+
shutil.copy(source_file, '.')
162+
module = Extension(filename, sources=[source_file.name])
163+
args = [
164+
'--verbose' if sys.flags.verbose else '--quiet',
165+
'build', '--build-temp=t', '--build-base=b', '--build-purelib=l', '--build-platlib=l',
166+
'install_lib', '-f', '--install-dir=.',
167+
]
168+
setup(
169+
script_name='setup',
170+
script_args=args,
171+
name=filename,
172+
version='1.0',
173+
description='',
174+
ext_modules=[module]
175+
)
176+
finally:
177+
os.chdir(old_cwd)
178+
179+
# write new checksum
180+
with open(checksum_file, "w") as f:
181+
f.write(cur_checksum)
182+
183+
# IMPORTANT:
184+
# Invalidate caches after creating the native module.
185+
# FileFinder caches directory contents, and the check for directory
186+
# changes has whole-second precision, so it can miss quick updates.
187+
invalidate_caches()
188+
189+
# ensure file was really written
190+
file_not_empty(lib_file)
191+
192+
return str(build_dir)
193+
194+
195+
def file_not_empty(path):
196+
for i in range(3):
197+
try:
198+
stat_result = os.stat(path)
199+
if stat_result[6] != 0:
200+
return
201+
except FileNotFoundError:
202+
pass
203+
raise SystemError("file %s not available" % path)

0 commit comments

Comments
 (0)