Skip to content

Commit ffd13cc

Browse files
committed
chore: Introduce vendored copies of cppllvm_build.py for ctidy and cformat packages to enable isolated builds. Update build_hooks.py to load these local copies, ensuring compatibility with Python 3.11 and enhancing package management across the repository.
1 parent 861a626 commit ffd13cc

8 files changed

Lines changed: 574 additions & 16 deletions

File tree

cppllvm_build.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from __future__ import annotations
22

3+
# Canonical shared build helper for cppllvm packages.
4+
# Package-local copies are vendored so each package can build in isolation.
5+
36
import os
47
import re
58
import shutil

packages/cformat/MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ include README.md
22
include pyproject.toml
33
include setup.py
44
include build_hooks.py
5+
include cppllvm_build.py
56
recursive-include src/cformat *.py
67
global-exclude __pycache__
78
global-exclude *.py[cod]

packages/cformat/build_hooks.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,23 @@
22

33
from __future__ import annotations
44

5+
import importlib.util
56
import sys
67
from pathlib import Path
78

8-
REPO_ROOT = Path(__file__).resolve().parents[2]
9-
sys.path.insert(0, str(REPO_ROOT))
10-
11-
from cppllvm_build import PackageBuildConfig, make_build_commands
12-
13-
149
ROOT = Path(__file__).resolve().parent
10+
MODULE_SPEC = importlib.util.spec_from_file_location(
11+
"_cformat_cppllvm_build",
12+
ROOT / "cppllvm_build.py",
13+
)
14+
if MODULE_SPEC is None or MODULE_SPEC.loader is None: # pragma: no cover
15+
raise RuntimeError("Failed to load cformat package build helpers.")
16+
BUILD_HELPERS = importlib.util.module_from_spec(MODULE_SPEC)
17+
sys.modules[MODULE_SPEC.name] = BUILD_HELPERS
18+
MODULE_SPEC.loader.exec_module(BUILD_HELPERS)
1519

16-
build_py, bdist_wheel = make_build_commands(
17-
PackageBuildConfig(
20+
build_py, bdist_wheel = BUILD_HELPERS.make_build_commands(
21+
BUILD_HELPERS.PackageBuildConfig(
1822
package_dir=ROOT,
1923
package_name="cformat",
2024
tool_section="cformat",

packages/cformat/cppllvm_build.py

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
from __future__ import annotations
2+
3+
# Vendored copy of /cppllvm_build.py.
4+
# This file stays package-local so `packages/cformat` can build in isolation.
5+
6+
import os
7+
import re
8+
import shutil
9+
import stat
10+
import sys
11+
import tarfile
12+
import urllib.request
13+
from dataclasses import dataclass
14+
from pathlib import Path
15+
from platform import machine, system
16+
17+
from setuptools.command.bdist_wheel import bdist_wheel as _bdist_wheel
18+
from setuptools.command.build_py import build_py as _build_py
19+
from setuptools.errors import SetupError
20+
21+
if sys.version_info >= (3, 11):
22+
import tomllib
23+
else: # pragma: no cover
24+
import tomli as tomllib
25+
26+
27+
SUPPORTED_PREBUILT_PLATFORMS: dict[tuple[str, str], tuple[str, str]] = {
28+
("Linux", "x86_64"): ("linux-amd64", ""),
29+
("Linux", "amd64"): ("linux-amd64", ""),
30+
("Darwin", "x86_64"): ("macosx-amd64", ""),
31+
("Darwin", "amd64"): ("macosx-amd64", ""),
32+
("Darwin", "arm64"): ("macos-arm-arm64", ""),
33+
("Darwin", "aarch64"): ("macos-arm-arm64", ""),
34+
("Windows", "x86_64"): ("windows-amd64", ".exe"),
35+
("Windows", "amd64"): ("windows-amd64", ".exe"),
36+
}
37+
38+
39+
@dataclass(frozen=True)
40+
class PackageBuildConfig:
41+
package_dir: Path
42+
package_name: str
43+
tool_section: str
44+
binaries: tuple[str, ...]
45+
copied_files: tuple[tuple[Path, str], ...] = ()
46+
include_resource_headers: bool = False
47+
download_env_vars: tuple[str, ...] = ()
48+
49+
50+
def pyproject_data(config: PackageBuildConfig) -> dict:
51+
pyproject = config.package_dir / "pyproject.toml"
52+
return tomllib.loads(pyproject.read_text(encoding="utf-8"))
53+
54+
55+
def project_version(config: PackageBuildConfig) -> str:
56+
return str(pyproject_data(config)["project"]["version"])
57+
58+
59+
def llvm_major_version(config: PackageBuildConfig) -> str:
60+
return project_version(config).split(".", 1)[0]
61+
62+
63+
def prebuilt_release_tag(config: PackageBuildConfig) -> str:
64+
return str(
65+
pyproject_data(config)["tool"][config.tool_section]["prebuilt_release_tag"]
66+
)
67+
68+
69+
def download_root(config: PackageBuildConfig) -> Path:
70+
for env_var in ("CPPLLVM_DOWNLOAD_DIR", *config.download_env_vars):
71+
value = os.environ.get(env_var)
72+
if value:
73+
return Path(value).resolve()
74+
return (
75+
config.package_dir / ".cache" / f"{config.package_name}-downloads"
76+
).resolve()
77+
78+
79+
def llvm_archive_path(config: PackageBuildConfig) -> Path:
80+
return (
81+
download_root(config)
82+
/ prebuilt_release_tag(config)
83+
/ f"llvm-project-{project_version(config)}.src.tar.xz"
84+
)
85+
86+
87+
def supported_platform_labels() -> str:
88+
return ", ".join(
89+
[
90+
"Linux/x86_64",
91+
"macOS/x86_64",
92+
"macOS/arm64",
93+
"Windows/x86_64",
94+
]
95+
)
96+
97+
98+
def current_platform(config: PackageBuildConfig) -> tuple[str, str]:
99+
host_system = system()
100+
host_machine = machine().lower()
101+
102+
platform_spec = SUPPORTED_PREBUILT_PLATFORMS.get((host_system, host_machine))
103+
if platform_spec is None:
104+
raise SetupError(
105+
f"{config.package_name} only publishes wheels for platforms with pinned "
106+
"prebuilt static binaries from muttleyxd/clang-tools-static-binaries. "
107+
f"Supported platforms for LLVM {llvm_major_version(config)}: "
108+
f"{supported_platform_labels()}. "
109+
f"Got {host_system}/{host_machine}."
110+
)
111+
return platform_spec
112+
113+
114+
def asset_name(config: PackageBuildConfig, stem: str) -> str:
115+
platform_name, suffix = current_platform(config)
116+
return f"{stem}-{llvm_major_version(config)}_{platform_name}{suffix}"
117+
118+
119+
def cached_download(url: str, destination: Path, *, package_name: str) -> Path:
120+
if destination.exists() and destination.stat().st_size > 0:
121+
return destination
122+
123+
destination.parent.mkdir(parents=True, exist_ok=True)
124+
request = urllib.request.Request(
125+
url,
126+
headers={"User-Agent": f"{package_name}-build-hooks"},
127+
)
128+
with urllib.request.urlopen(request) as response, destination.open("wb") as handle:
129+
shutil.copyfileobj(response, handle)
130+
return destination
131+
132+
133+
def read_expected_sha512(path: Path) -> str:
134+
content = path.read_text(encoding="utf-8").strip()
135+
match = re.match(r"(?P<hash>[0-9A-Fa-f]+)", content)
136+
if match is None:
137+
raise SetupError(f"Could not parse SHA-512 from {path}.")
138+
return match.group("hash").lower()
139+
140+
141+
def sha512(path: Path) -> str:
142+
import hashlib
143+
144+
digest = hashlib.sha512()
145+
with path.open("rb") as handle:
146+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
147+
digest.update(chunk)
148+
return digest.hexdigest().lower()
149+
150+
151+
def download_prebuilt_asset(config: PackageBuildConfig, stem: str) -> Path:
152+
release_tag = prebuilt_release_tag(config)
153+
asset = asset_name(config, stem)
154+
asset_path = download_root(config) / release_tag / asset
155+
base_url = (
156+
"https://github.com/muttleyxd/clang-tools-static-binaries/releases/download/"
157+
f"{release_tag}/{asset}"
158+
)
159+
hash_path = asset_path.with_name(f"{asset}.sha512sum")
160+
161+
cached_download(base_url, asset_path, package_name=config.package_name)
162+
cached_download(
163+
f"{base_url}.sha512sum",
164+
hash_path,
165+
package_name=config.package_name,
166+
)
167+
168+
expected = read_expected_sha512(hash_path)
169+
actual = sha512(asset_path)
170+
if actual != expected:
171+
raise SetupError(
172+
f"SHA-512 mismatch for {asset}: expected {expected}, got {actual}."
173+
)
174+
175+
return asset_path
176+
177+
178+
def copy_executable(source: Path, destination: Path) -> None:
179+
destination.parent.mkdir(parents=True, exist_ok=True)
180+
shutil.copy2(source, destination)
181+
destination.chmod(
182+
destination.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
183+
)
184+
185+
186+
def extract_resource_headers(config: PackageBuildConfig, destination: Path) -> None:
187+
version = project_version(config)
188+
archive = llvm_archive_path(config)
189+
url = (
190+
"https://github.com/llvm/llvm-project/releases/download/"
191+
f"llvmorg-{version}/llvm-project-{version}.src.tar.xz"
192+
)
193+
cached_download(url, archive, package_name=config.package_name)
194+
195+
prefix = f"llvm-project-{version}.src/clang/lib/Headers/"
196+
if destination.exists():
197+
shutil.rmtree(destination)
198+
destination.mkdir(parents=True, exist_ok=True)
199+
200+
with tarfile.open(archive, mode="r:xz") as tar:
201+
for member in tar.getmembers():
202+
if not member.name.startswith(prefix):
203+
continue
204+
relative = member.name[len(prefix) :]
205+
if not relative or relative == "CMakeLists.txt":
206+
continue
207+
208+
output = destination / relative
209+
if member.isdir():
210+
output.mkdir(parents=True, exist_ok=True)
211+
continue
212+
213+
output.parent.mkdir(parents=True, exist_ok=True)
214+
extracted = tar.extractfile(member)
215+
if extracted is None:
216+
continue
217+
with extracted, output.open("wb") as handle:
218+
shutil.copyfileobj(extracted, handle)
219+
220+
221+
def stage_payload(config: PackageBuildConfig, build_lib: Path) -> None:
222+
_, executable_suffix = current_platform(config)
223+
package_root = build_lib / config.package_name
224+
data_root = package_root / "data"
225+
bin_dir = data_root / "bin"
226+
227+
for source, relative_destination in config.copied_files:
228+
destination = data_root / relative_destination
229+
destination.parent.mkdir(parents=True, exist_ok=True)
230+
shutil.copy2(source, destination)
231+
232+
for stem in config.binaries:
233+
copy_executable(
234+
download_prebuilt_asset(config, stem),
235+
bin_dir / f"{stem}{executable_suffix}",
236+
)
237+
238+
if config.include_resource_headers:
239+
extract_resource_headers(
240+
config,
241+
data_root / "lib" / "clang" / llvm_major_version(config) / "include",
242+
)
243+
244+
245+
def make_build_commands(
246+
config: PackageBuildConfig,
247+
) -> tuple[type[_build_py], type[_bdist_wheel]]:
248+
class build_py(_build_py):
249+
def run(self) -> None:
250+
super().run()
251+
stage_payload(config, Path(self.build_lib))
252+
253+
class bdist_wheel(_bdist_wheel):
254+
def finalize_options(self) -> None:
255+
super().finalize_options()
256+
self.root_is_pure = False
257+
258+
def get_tag(self) -> tuple[str, str, str]:
259+
_, _, platform_tag = super().get_tag()
260+
return "py3", "none", platform_tag
261+
262+
return build_py, bdist_wheel

packages/ctidy/MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ include README.md
22
include pyproject.toml
33
include setup.py
44
include build_hooks.py
5+
include cppllvm_build.py
56
recursive-include src/ctidy *.py
67
recursive-include src/ctidy/data/bin run-clang-tidy.py
78
global-exclude __pycache__

packages/ctidy/build_hooks.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
# ruff: noqa: E402,I001
22

3+
import importlib.util
34
import sys
45
from pathlib import Path
56

6-
REPO_ROOT = Path(__file__).resolve().parents[2]
7-
sys.path.insert(0, str(REPO_ROOT))
8-
9-
from cppllvm_build import PackageBuildConfig, make_build_commands
10-
11-
127
ROOT = Path(__file__).resolve().parent
8+
MODULE_SPEC = importlib.util.spec_from_file_location(
9+
"_ctidy_cppllvm_build",
10+
ROOT / "cppllvm_build.py",
11+
)
12+
if MODULE_SPEC is None or MODULE_SPEC.loader is None: # pragma: no cover
13+
raise RuntimeError("Failed to load ctidy package build helpers.")
14+
BUILD_HELPERS = importlib.util.module_from_spec(MODULE_SPEC)
15+
sys.modules[MODULE_SPEC.name] = BUILD_HELPERS
16+
MODULE_SPEC.loader.exec_module(BUILD_HELPERS)
1317

14-
build_py, bdist_wheel = make_build_commands(
15-
PackageBuildConfig(
18+
build_py, bdist_wheel = BUILD_HELPERS.make_build_commands(
19+
BUILD_HELPERS.PackageBuildConfig(
1620
package_dir=ROOT,
1721
package_name="ctidy",
1822
tool_section="ctidy",

0 commit comments

Comments
 (0)