File size: 5,685 Bytes
f0023cf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
"""
build_engine.py
~~~~~~~~~~~~~~~
Cross-platform build script for the ``hft_auditor`` C++ extension module.

Usage
-----
    python build_engine.py

The script:
  1. Detects the host OS (Linux/WSL or Windows).
  2. Cleans any previous build artefacts.
  3. Runs CMake configure + build inside ``hf auditor/build/``.
  4. Locates the compiled shared library (.so / .pyd) and copies it to the
     project root so that ``import hft_auditor`` works from any script in
     this directory.
"""

import os
import platform
import shutil
import subprocess
import sys
import argparse
from pathlib import Path

# ---------------------------------------------------------------------------
# Paths
# ---------------------------------------------------------------------------
ROOT = Path(__file__).resolve().parent  # project root  (fin_auditor/)
SOURCE_DIR = ROOT / "hf auditor"  # note: directory has a space
BUILD_DIR = SOURCE_DIR / "build"

# Glob patterns for the compiled extension in the build tree
SO_PATTERNS = ["hft_auditor*.so", "hft_auditor*.pyd"]

# Destination files that should be removed from the project root on clean
ROOT_ARTEFACTS = list(ROOT.glob("hft_auditor*.so")) + list(
    ROOT.glob("hft_auditor*.pyd")
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def run(cmd: list[str], *, description: str = "") -> None:
    """Run *cmd* with real-time stdout/stderr passthrough."""
    if description:
        print(f"\n{'=' * 60}")
        print(f"  {description}")
        print(f"{'=' * 60}")

    print(f"$ {' '.join(cmd)}\n")
    result = subprocess.run(cmd, check=True)  # noqa: S603
    return result


def clean() -> None:
    """Remove existing build artefacts."""
    print("\n-- Clean -----------------------------------------------------")

    if BUILD_DIR.exists():
        print(f"  Removing build directory: {BUILD_DIR}")

        def remove_readonly(func, path, excinfo):
            os.chmod(path, 0o777)
            func(path)

        shutil.rmtree(BUILD_DIR, onerror=remove_readonly)
    else:
        print(f"  Build directory not found (nothing to remove): {BUILD_DIR}")

    for path in list(ROOT.glob("hft_auditor*.so")) + list(
        ROOT.glob("hft_auditor*.pyd")
    ):
        print(f"  Removing root artefact: {path}")
        path.unlink()

    print("  Clean complete.\n")


def cmake_configure(os_name: str, docker_safe: bool = False) -> None:
    """Run the CMake configure step."""
    cmd = [
        "cmake",
        "-B",
        str(BUILD_DIR),
        "-S",
        str(SOURCE_DIR),
        f"-DCMAKE_BUILD_TYPE=Release",
    ]

    if docker_safe:
        # -O1 peaks at ~1.2 GB vs -O3's ~5 GB for this translation unit.
        # DOCKER_SAFE_BUILD=ON triggers the matching CMakeLists.txt option.
        cmd += ["-DDOCKER_SAFE_BUILD=ON", "-DCMAKE_CXX_FLAGS=-O1"]
        print("  Memory-safe mode: -j1, -O1, DOCKER_SAFE_BUILD=ON")

    if os_name == "Windows":
        cmd += ["-G", "Visual Studio 17 2022"]

    run(cmd, description="CMake – Configure")


def cmake_build(docker_safe: bool = False) -> None:
    """Run the CMake build step."""
    cmd = ["cmake", "--build", str(BUILD_DIR), "--config", "Release"]
    if docker_safe:
        # --parallel 1  →  make -j1  →  caps peak RAM to ~1.5 GB
        cmd += ["--parallel", "1"]
    run(cmd, description="CMake – Build")


def copy_to_root() -> Path:
    """
    Walk the build tree looking for the compiled extension and copy it to the
    project root.  Returns the destination path.
    """
    print("\n-- Post-Build: Locate & Copy Extension -----------------------")

    found: list[Path] = []
    for pattern in SO_PATTERNS:
        found.extend(BUILD_DIR.rglob(pattern))

    if not found:
        sys.exit(
            "ERROR: Could not find a compiled hft_auditor extension under "
            f"{BUILD_DIR}.  Did the build succeed?"
        )

    # If somehow multiple matches exist (e.g. debug + release), prefer the
    # one in a 'Release' sub-directory; otherwise take the first hit.
    chosen = next(
        (p for p in found if "Release" in p.parts or "release" in p.parts),
        found[0],
    )

    dest = ROOT / chosen.name
    print(f"  Source : {chosen}")
    print(f"  Dest   : {dest}")
    shutil.copy2(chosen, dest)
    print(f"  Copied '{chosen.name}' → project root.")
    return dest


# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def main() -> None:
    parser = argparse.ArgumentParser(description="Build the hft_auditor C++ extension.")
    parser.add_argument(
        "--docker-safe",
        action="store_true",
        help="Memory-safe build for Docker: -j1, -O1 instead of -O3 -march=native.",
    )
    args = parser.parse_args()

    os_name = platform.system()  # 'Linux', 'Windows', or 'Darwin'
    print(f"Detected OS : {os_name}")
    if args.docker_safe:
        print("Mode       : DOCKER_SAFE (--parallel 1, -O1)")

    if os_name not in ("Linux", "Windows"):
        print(
            f"WARNING: OS '{os_name}' is not explicitly supported.  Proceeding "
            "with generic Unix-style CMake commands."
        )

    # 1. Clean
    clean()

    # 2. Configure
    cmake_configure(os_name, docker_safe=args.docker_safe)

    # 3. Build
    cmake_build(docker_safe=args.docker_safe)

    # 4. Copy extension to project root
    dest = copy_to_root()

    print(f"\n✓  Build complete.  Extension available at: {dest}\n")


if __name__ == "__main__":
    main()