Skip to content

Commit ee2c9b2

Browse files
authored
Prioritize cmake and fix cmake check (#16)
* ruff * always clean old builds for src builds * prioritize cmake * refractor * update
1 parent 283e8e4 commit ee2c9b2

5 files changed

Lines changed: 678 additions & 264 deletions

File tree

README.md

Lines changed: 38 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,22 @@
99

1010
Python bindings for the system PCRE2 library with a familiar `re`-style API.
1111

12+
<p align="center">
13+
<a href="https://github.com/ModelCloud/PyPcre/releases" style="text-decoration:none;"><img alt="GitHub release" src="https://img.shields.io/github/release/ModelCloud/Pcre.svg"></a>
14+
<a href="https://pypi.org/project/PyPcre/" style="text-decoration:none;"><img alt="PyPI - Version" src="https://img.shields.io/pypi/v/PyPcre"></a>
15+
<!-- <a href="https://pepy.tech/projects/PyPcre" style="text-decoration:none;"><img src="https://static.pepy.tech/badge/PyPcre" alt="PyPI Downloads"></a> -->
16+
<a href="https://github.com/ModelCloud/PyPcre/blob/main/LICENSE"><img src="https://img.shields.io/pypi/l/PyPcre"></a>
17+
<a href="https://huggingface.co/modelcloud/"><img src="https://img.shields.io/badge/🤗%20Hugging%20Face-ModelCloud-%23ff8811.svg"></a>
18+
</p>
19+
20+
1221
## Installation
1322

1423
```bash
1524
pip install PyPcre
1625
```
1726

18-
The package links against the `libpcre2-8` variant already available on your
19-
system. See [Building](#building) for manual build details.
27+
The package prioritizes linking against the `libpcre2-8` shared library in system for fast install and max security protection which gets latest patches from OS. See [Building](#building) for manual build details.
2028

2129
## Usage
2230

@@ -78,16 +86,10 @@ the conversion without repeating the flag.
7886
### Automatic pattern caching
7987

8088
`pcre.compile()` caches the final `Pattern` wrapper for up to 128
81-
unique `(pattern, flags)` pairs when the pattern object is hashable. This
82-
keeps repeated calls to top-level helpers efficient without any extra work
83-
from the caller. Adjust the capacity with `pcre.set_cache_limit(n)`—pass
89+
unique `(pattern, flags)` pairs when the pattern object is hashable. Adjust the capacity with `pcre.set_cache_limit(n)`—pass
8490
`0` to disable caching completely or `None` for an unlimited cache—and
8591
check the current limit with `pcre.get_cache_limit()`. The cache can be
86-
emptied at any time with `pcre.clear_cache()` if your application needs to
87-
release memory proactively.
88-
89-
Non-hashable patterns (for example, custom objects) bypass the cache and are
90-
still compiled immediately.
92+
emptied at any time with `pcre.clear_cache()`.
9193

9294
### Text versus bytes defaults
9395

@@ -116,7 +118,7 @@ bytes.
116118
order of the provided subjects and returns the same result objects you’d
117119
normally receive from the `Pattern` methods.
118120
- Threading is **opt-in by default** when Python runs without the GIL
119-
(e.g. CPython with `-X gil=0` or `PYTHON_GIL=0`). When the GIL is active the default falls
121+
(e.g. Python with `-X gil=0` or `PYTHON_GIL=0`). When the GIL is active the default falls
120122
back to sequential execution to avoid needless overhead.
121123
- With auto threading enabled (`configure_threads(enabled=True)`), the pool
122124
is only engaged when at least one subject is larger than the configured
@@ -131,9 +133,9 @@ bytes.
131133
`preload=True` to spin the pool up eagerly, and `shutdown_thread_pool()`
132134
to tear it down manually if needed.
133135

134-
### JIT control
136+
### JIT Pattern Compilation and Execution
135137

136-
Pcre’s JIT compiler is enabled by default for every compiled pattern. The
138+
Pcre2’s JIT compiler is enabled by default for every compiled pattern. The
137139
wrapper exposes two complementary ways to adjust that behaviour:
138140

139141
- Toggle the global default at runtime with `pcre.configure(jit=False)` to
@@ -151,6 +153,29 @@ wrapper exposes two complementary ways to adjust that behaviour:
151153
slow = compile(r"expr", flags=Flag.NO_JIT) # force-disable for this pattern
152154
```
153155

156+
## Pattern cache
157+
- `pcre.compile()` caches hashable `(pattern, flags)` pairs, keeping up to 128 entries.
158+
- Use `pcre.clear_cache()` when you need to free the cache proactively.
159+
- Non-hashable pattern objects skip the cache and are compiled each time.
160+
161+
## Default flags for text patterns
162+
- String patterns enable `Flag.UTF` and `Flag.UCP` automatically so behaviour matches `re`.
163+
- Byte patterns keep both flags disabled; opt in manually if Unicode semantics are desired.
164+
- Explicitly supply `Flag.NO_UTF`/`Flag.NO_UCP` to override the defaults for strings.
165+
166+
## Additional usage notes
167+
- All top-level helpers (`match`, `search`, `fullmatch`, `finditer`, `findall`) defer to the cached compiler.
168+
- Compiled `Pattern` objects expose `.pattern`, `.flags`, `.jit`, and `.groupindex` for introspection.
169+
- Execution helpers accept `pos`, `endpos`, and `options`, allowing you to thread PCRE2 execution flags per call.
170+
171+
## Memory allocation
172+
- PyPcre will select the fastest available allocator at import time: it
173+
prefers jemalloc, then tcmalloc, and finally falls back to the platform
174+
`malloc`. Optional allocators are loaded via `dlopen`, so no additional
175+
link flags are required when they are absent.
176+
- Call `pcre_ext_c.get_allocator()` to inspect which backend is active at
177+
runtime.
178+
154179
## Building
155180

156181
The extension links against an existing PCRE2 installation (the `libpcre2-8`
@@ -182,31 +207,3 @@ If your system ships `libpcre2-8` under `/usr` but you also maintain a
182207
manually built copy under `/usr/local`, export `PCRE2_LIBRARY_PATH` (and, if
183208
needed, a matching `PCRE2_INCLUDE_DIR`) so the build links against the desired
184209
location.
185-
186-
# Notes
187-
188-
## Pattern cache
189-
- `pcre.compile()` caches hashable `(pattern, flags)` pairs, keeping up to 128 entries.
190-
- Use `pcre.clear_cache()` when you need to free the cache proactively.
191-
- Non-hashable pattern objects skip the cache and are compiled each time.
192-
193-
## Default flags for text patterns
194-
- String patterns enable `Flag.UTF` and `Flag.UCP` automatically so behaviour matches `re`.
195-
- Byte patterns keep both flags disabled; opt in manually if Unicode semantics are desired.
196-
- Explicitly supply `Flag.NO_UTF`/`Flag.NO_UCP` to override the defaults for strings.
197-
198-
## Additional usage notes
199-
- All top-level helpers (`match`, `search`, `fullmatch`, `finditer`, `findall`) defer to the cached compiler.
200-
- Compiled `Pattern` objects expose `.pattern`, `.flags`, `.jit`, and `.groupindex` for introspection.
201-
- Execution helpers accept `pos`, `endpos`, and `options`, allowing you to thread PCRE2 execution flags per call.
202-
203-
## Memory allocation
204-
- The extension selects the fastest available allocator at import time: it
205-
prefers jemalloc, then tcmalloc, and finally falls back to the platform
206-
`malloc`. Optional allocators are loaded via `dlopen`, so no additional
207-
link flags are required when they are absent.
208-
- All internal buffers (match data wrappers, JIT stack cache entries, error
209-
formatting scratch space) use the chosen allocator; CPython’s `PyMem_*`
210-
family is no longer used within the extension.
211-
- Call `pcre_ext_c.get_allocator()` to inspect which backend is active at
212-
runtime.

pcre/__init__.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import pcre_ext_c as _backend
2020

21+
2122
pcre_ext_c = _backend
2223
from .cache import get_cache_limit, set_cache_limit
2324
from .flags import PY_ONLY_FLAG_MEMBERS
@@ -39,9 +40,7 @@
3940
sub,
4041
subn,
4142
)
42-
43-
from .threads import configure_thread_pool, shutdown_thread_pool
44-
from .threads import configure_threads
43+
from .threads import configure_thread_pool, configure_threads, shutdown_thread_pool
4544

4645

4746
__version__ = getattr(_backend, "__version__", "0.0")

pcre/pcre.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from typing import Any, List
1414

1515
import pcre_ext_c as _pcre2
16+
1617
from .cache import cached_compile
1718
from .cache import clear_cache as _clear_cache
1819
from .flags import (

setup.py

Lines changed: 217 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,233 @@
55

66
from __future__ import annotations
77

8+
import os
9+
import shlex
810
import sys
911
from pathlib import Path
1012

1113
from setuptools import Extension, setup
1214
from setuptools.command.build_ext import build_ext
1315

16+
1417
ROOT_DIR = Path(__file__).resolve().parent
1518
if str(ROOT_DIR) not in sys.path:
1619
sys.path.insert(0, str(ROOT_DIR))
1720

18-
from setup_utils import MODULE_SOURCES, collect_build_config
21+
import setup_utils
22+
from setup_utils import (
23+
augment_compile_flags,
24+
discover_include_dirs,
25+
discover_library_dirs,
26+
extend_env_paths,
27+
extend_unique,
28+
find_library_with_brew,
29+
find_library_with_ldconfig,
30+
find_library_with_pkg_config,
31+
has_header,
32+
has_library,
33+
is_truthy_env,
34+
is_windows_platform,
35+
locate_library_file,
36+
prepare_pcre2_source,
37+
run_pkg_config,
38+
)
39+
40+
41+
MODULE_SOURCES = [
42+
"pcre_ext/pcre2.c",
43+
"pcre_ext/error.c",
44+
"pcre_ext/cache.c",
45+
"pcre_ext/flag.c",
46+
"pcre_ext/util.c",
47+
"pcre_ext/memory.c",
48+
]
49+
50+
PCRE_EXT_DIR = ROOT_DIR / "pcre_ext"
51+
PCRE2_REPO_URL = "https://github.com/PCRE2Project/pcre2.git"
52+
PCRE2_TAG = "pcre2-10.46"
53+
54+
LIB_EXTENSIONS = [
55+
".so",
56+
".so.0",
57+
".so.1",
58+
".a",
59+
".dylib",
60+
".sl",
61+
]
62+
63+
LIBRARY_BASENAME = "libpcre2-8"
64+
65+
LIBRARY_SEARCH_PATTERNS = [
66+
f"**/{LIBRARY_BASENAME}.lib",
67+
f"**/{LIBRARY_BASENAME}.a",
68+
f"**/{LIBRARY_BASENAME}.so",
69+
f"**/{LIBRARY_BASENAME}.so.*",
70+
f"**/{LIBRARY_BASENAME}.dylib",
71+
"**/pcre2-8.lib",
72+
"**/pcre2-8.dll",
73+
"**/pcre2-8-static.lib",
74+
"**/pcre2-8-static.dll",
75+
]
76+
77+
RUNTIME_LIBRARY_FILES: list[str] = []
78+
79+
setup_utils.configure_environment(
80+
pcre_ext_dir=PCRE_EXT_DIR,
81+
repo_url=PCRE2_REPO_URL,
82+
repo_tag=PCRE2_TAG,
83+
lib_extensions=LIB_EXTENSIONS,
84+
library_basename=LIBRARY_BASENAME,
85+
library_search_patterns=LIBRARY_SEARCH_PATTERNS,
86+
)
87+
88+
89+
def collect_build_config() -> dict[str, list[str] | list[tuple[str, str | None]]]:
90+
include_dirs: list[str] = []
91+
library_dirs: list[str] = []
92+
libraries: list[str] = []
93+
extra_compile_args: list[str] = []
94+
extra_link_args: list[str] = []
95+
define_macros: list[tuple[str, str | None]] = []
96+
library_files: list[str] = []
97+
98+
source_include_dirs, source_library_dirs, source_library_files = prepare_pcre2_source()
99+
for directory in source_include_dirs:
100+
extend_unique(include_dirs, directory)
101+
for directory in source_library_dirs:
102+
extend_unique(library_dirs, directory)
103+
for path in source_library_files:
104+
extend_unique(library_files, path)
105+
106+
cflags = run_pkg_config("--cflags")
107+
libs = run_pkg_config("--libs")
108+
109+
for flag in cflags:
110+
if flag.startswith("-I") and len(flag) > 2:
111+
extend_unique(include_dirs, flag[2:])
112+
elif flag.startswith("-D") and len(flag) > 2:
113+
name_value = flag[2:].split("=", 1)
114+
define_macros.append((name_value[0], name_value[1] if len(name_value) > 1 else None))
115+
else:
116+
extra_compile_args.append(flag)
117+
118+
for flag in libs:
119+
if flag.startswith("-L") and len(flag) > 2:
120+
extend_unique(library_dirs, flag[2:])
121+
elif flag.startswith("-l") and len(flag) > 2:
122+
extend_unique(libraries, flag[2:])
123+
else:
124+
extra_link_args.append(flag)
125+
126+
extend_env_paths(include_dirs, "PCRE2_INCLUDE_DIR")
127+
extend_env_paths(library_dirs, "PCRE2_LIBRARY_DIR")
128+
129+
env_lib_path = os.environ.get("PCRE2_LIBRARY_PATH")
130+
if env_lib_path:
131+
for raw_path in env_lib_path.split(os.pathsep):
132+
candidate = raw_path.strip()
133+
if not candidate:
134+
continue
135+
path = Path(candidate)
136+
if path.is_file() or any(candidate.endswith(ext) for ext in LIB_EXTENSIONS):
137+
extend_unique(library_files, str(path))
138+
parent = str(path.parent)
139+
if parent:
140+
extend_unique(library_dirs, parent)
141+
else:
142+
extend_unique(library_dirs, candidate)
143+
144+
extend_env_paths(libraries, "PCRE2_LIBRARIES")
145+
146+
if not library_files:
147+
for path in find_library_with_pkg_config():
148+
extend_unique(library_files, path)
149+
150+
if not library_files:
151+
directory_candidates = [Path(p) for p in library_dirs]
152+
directory_candidates.extend(Path(p) for p in discover_library_dirs())
153+
for directory in directory_candidates:
154+
located = locate_library_file(directory)
155+
if located is not None:
156+
extend_unique(library_files, str(located))
157+
break
158+
159+
if not library_files:
160+
for path in find_library_with_ldconfig():
161+
extend_unique(library_files, path)
162+
163+
if not library_files:
164+
for path in find_library_with_brew():
165+
extend_unique(library_files, path)
166+
167+
env_cflags = os.environ.get("PCRE2_CFLAGS")
168+
if env_cflags:
169+
extra_compile_args.extend(shlex.split(env_cflags))
170+
171+
env_ldflags = os.environ.get("PCRE2_LDFLAGS")
172+
if env_ldflags:
173+
extra_link_args.extend(shlex.split(env_ldflags))
174+
175+
if not is_windows_platform() and not any(flag.startswith("-std=") for flag in extra_compile_args):
176+
extra_compile_args.append("-std=c99")
177+
178+
if not has_header(include_dirs):
179+
include_dirs.extend(discover_include_dirs())
180+
181+
if not has_library(library_dirs):
182+
library_dirs.extend(discover_library_dirs())
183+
184+
runtime_libraries: list[str] = []
185+
186+
if library_files:
187+
for runtime_path in library_files:
188+
lower_name = runtime_path.lower()
189+
if lower_name.endswith(".dll") or lower_name.endswith(".dylib") or ".so" in Path(runtime_path).name:
190+
extend_unique(runtime_libraries, runtime_path)
191+
192+
linkable_files: list[str] = []
193+
for path in library_files:
194+
suffix = Path(path).suffix.lower()
195+
if suffix == ".dll":
196+
continue
197+
linkable_files.append(path)
198+
199+
if linkable_files:
200+
libraries = [lib for lib in libraries if lib != "pcre2-8"]
201+
for path in linkable_files:
202+
extend_unique(extra_link_args, path)
203+
parent = str(Path(path).parent)
204+
if parent:
205+
extend_unique(library_dirs, parent)
206+
elif "pcre2-8" not in libraries:
207+
libraries.append("pcre2-8")
208+
elif "pcre2-8" not in libraries:
209+
libraries.append("pcre2-8")
210+
211+
if sys.platform.startswith("linux") and "dl" not in libraries:
212+
libraries.append("dl")
213+
214+
if is_windows_platform():
215+
has_runtime_dll = any(path.lower().endswith(".dll") for path in runtime_libraries)
216+
force_static_env = is_truthy_env("PCRE2_FORCE_STATIC")
217+
if (force_static_env or (library_files and not has_runtime_dll)) and not any(
218+
macro[0] == "PCRE2_STATIC" for macro in define_macros
219+
):
220+
define_macros.append(("PCRE2_STATIC", "1"))
221+
222+
augment_compile_flags(extra_compile_args)
223+
224+
RUNTIME_LIBRARY_FILES.clear()
225+
RUNTIME_LIBRARY_FILES.extend(runtime_libraries)
226+
227+
return {
228+
"include_dirs": include_dirs,
229+
"library_dirs": library_dirs,
230+
"libraries": libraries,
231+
"extra_compile_args": extra_compile_args,
232+
"extra_link_args": extra_link_args,
233+
"define_macros": define_macros,
234+
}
19235

20236

21237
EXTENSION = Extension(

0 commit comments

Comments
 (0)