Skip to content

Commit fbf5d90

Browse files
Centralize native library builds in Python script
1 parent 37ffb1c commit fbf5d90

4 files changed

Lines changed: 255 additions & 21 deletions

File tree

.github/workflows/release-binaries.yml

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -274,28 +274,10 @@ jobs:
274274
python -m pip install -e .
275275
276276
- name: Build RepTate native macOS libraries (${{ matrix.architecture }})
277-
shell: bash
277+
env:
278+
MACOSX_DEPLOYMENT_TARGET: ${{ env.MACOS_MIN_VERSION }}
278279
run: |
279-
set -euo pipefail
280-
export MACOSX_DEPLOYMENT_TARGET="${{ env.MACOS_MIN_VERSION }}"
281-
282-
# These commands reproduce the source README recipes with Apple's
283-
# native C/C++ compiler. Outputs intentionally replace only the
284-
# Darwin libraries; Windows/Linux artifacts are untouched.
285-
(cd RepTate/theories/dtd_source_c_code && cc -dynamiclib -fPIC -O2 dtd.c trapz.c qtrap.c -o ../dtd_lib_darwin.so)
286-
(cd RepTate/theories/kww_source_c_code && cc -dynamiclib -fPIC -O2 kww.c -o ../kww_lib_darwin.so)
287-
(cd RepTate/theories/BuildLandscape_Linux && cc -dynamiclib -fPIC -O2 *.c -I./ -o ../landscape_darwin.so)
288-
(cd RepTate/theories/react_source_c_code && cc -dynamiclib -fPIC -O2 binsandbob.c polybits.c polycleanup.c polymassrg.c ran3.c tobitabatch.c tobitaCSTR.c multimetCSTR.c dieneCSTR.c calc_architecture.c -o ../react_lib_darwin.so)
289-
(cd RepTate/theories/rouse_source_c_code && cc -dynamiclib -fPIC -O2 rouse.c -o ../rouse_lib_darwin.so)
290-
(cd RepTate/theories/rp_blend_source_c_code && cc -dynamiclib -fPIC -O2 derivs_rolie_poly_blend.c -o ../rp_blend_lib_darwin.so)
291-
(cd RepTate/theories/sccr_source_c_code && cc -dynamiclib -fPIC -O2 sccr.c -o ../sccr_lib_darwin.so)
292-
(cd RepTate/theories/schwarzl_source_c_code && cc -dynamiclib -fPIC -O2 schwarzl.c -o ../schwarzl_lib_darwin.so)
293-
294-
# Bob is the only C++ library and has its own generated-object
295-
# makefile, documented in modified_bob2.5/README.txt.
296-
make -C RepTate/theories/modified_bob2.5/code/src/obj -f makefile_for_lib clean
297-
make -C RepTate/theories/modified_bob2.5/code/src/obj -f makefile_for_lib
298-
cp RepTate/theories/modified_bob2.5/code/src/obj/bob2p5_lib.so RepTate/theories/bob2p5_lib_darwin.so
280+
python scripts/build_native_libraries.py
299281
300282
- name: Audit RepTate native libraries before Nuitka
301283
shell: bash

docs/source/developers/developers.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ RepTate for developers
44

55
Contents:
66

7+
Native theory libraries
8+
------------------------
9+
10+
Source developers can rebuild the RepTate-owned native theory libraries with::
11+
12+
python scripts/build_native_libraries.py
13+
14+
Release CI uses this same entry point for native macOS builds. Ordinary users
15+
installing a binary release do not need to compile these libraries.
16+
717
.. toctree::
818
:maxdepth: 2
919

scripts/build_native_libraries.py

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
"""Build RepTate-owned native theory libraries.
2+
3+
The Darwin path is used by the release workflow and is also useful for source
4+
developers. Linux and Windows entries are intentionally not activated yet.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import argparse
10+
import os
11+
import platform as platform_module
12+
import shutil
13+
import subprocess
14+
import sys
15+
from dataclasses import dataclass
16+
from pathlib import Path
17+
from typing import Sequence
18+
19+
20+
@dataclass(frozen=True)
21+
class NativeLibrary:
22+
name: str
23+
source_dir: str
24+
source_files: tuple[str, ...]
25+
output_name: str
26+
special_build: str | None = None
27+
include_current_directory: bool = False
28+
29+
def source_path(self, theories_dir: Path) -> Path:
30+
return theories_dir / self.source_dir
31+
32+
def output_path(self, theories_dir: Path) -> Path:
33+
return theories_dir / self.output_name
34+
35+
36+
_LANDSCAPE_SOURCES = (
37+
"brent.c", "c_math.c", "convergence.c", "error.c", "fdfsolver.c",
38+
"fdiv.c", "fsolver.c", "infnan.c", "landscape.c", "newton.c",
39+
"pow_int.c", "stream.c",
40+
)
41+
42+
NATIVE_LIBRARIES = (
43+
NativeLibrary("dtd", "dtd_source_c_code", ("dtd.c", "trapz.c", "qtrap.c"), "dtd_lib_darwin.so"),
44+
NativeLibrary("kww", "kww_source_c_code", ("kww.c",), "kww_lib_darwin.so"),
45+
NativeLibrary("landscape", "BuildLandscape_Linux", _LANDSCAPE_SOURCES, "landscape_darwin.so", include_current_directory=True),
46+
NativeLibrary(
47+
"react", "react_source_c_code",
48+
("binsandbob.c", "polybits.c", "polycleanup.c", "polymassrg.c", "ran3.c", "tobitabatch.c", "tobitaCSTR.c", "multimetCSTR.c", "dieneCSTR.c", "calc_architecture.c"),
49+
"react_lib_darwin.so",
50+
),
51+
NativeLibrary("rouse", "rouse_source_c_code", ("rouse.c",), "rouse_lib_darwin.so"),
52+
NativeLibrary("rp_blend", "rp_blend_source_c_code", ("derivs_rolie_poly_blend.c",), "rp_blend_lib_darwin.so"),
53+
NativeLibrary("sccr", "sccr_source_c_code", ("sccr.c",), "sccr_lib_darwin.so"),
54+
NativeLibrary("schwarzl", "schwarzl_source_c_code", ("schwarzl.c",), "schwarzl_lib_darwin.so"),
55+
NativeLibrary("bob", "modified_bob2.5/code/src/obj", (), "bob2p5_lib_darwin.so", special_build="bob_makefile"),
56+
)
57+
58+
59+
class BuildError(RuntimeError):
60+
"""An actionable native-library build or validation error."""
61+
62+
63+
def detect_platform(sys_platform: str | None = None) -> str:
64+
"""Return the repository's normalized platform identity."""
65+
value = sys_platform if sys_platform is not None else sys.platform
66+
if value == "darwin":
67+
return "darwin"
68+
if value.startswith("linux"):
69+
return "linux"
70+
if value.startswith("win"):
71+
return "windows"
72+
return value
73+
74+
75+
def expected_architecture() -> str:
76+
value = platform_module.machine().lower()
77+
return {"amd64": "x86_64", "x86-64": "x86_64"}.get(value, value)
78+
79+
80+
def parse_architectures(output: str) -> tuple[str, ...]:
81+
"""Parse the whitespace-separated architecture list printed by lipo."""
82+
return tuple(part for part in output.split() if part)
83+
84+
85+
def direct_command(library: NativeLibrary, compiler: str = "cc") -> list[str]:
86+
command = [compiler, "-dynamiclib", "-fPIC", "-O2", *library.source_files]
87+
if library.include_current_directory:
88+
command.extend(["-I./"])
89+
command.extend(["-o", f"../{library.output_name}"])
90+
return command
91+
92+
93+
def _run(command: Sequence[str], cwd: Path, library: NativeLibrary, verbose: bool) -> None:
94+
if verbose:
95+
print("$", " ".join(command), f"(in {cwd})")
96+
try:
97+
subprocess.run(command, cwd=cwd, check=True)
98+
except FileNotFoundError as exc:
99+
raise BuildError(f"{library.name}: command not found: {command[0]} (source: {cwd})") from exc
100+
except subprocess.CalledProcessError as exc:
101+
raise BuildError(
102+
f"{library.name}: command failed with exit status {exc.returncode}: "
103+
f"{' '.join(command)} (source: {cwd})"
104+
) from exc
105+
106+
107+
def verify_architecture(path: Path, expected: str) -> None:
108+
try:
109+
result = subprocess.run(["lipo", "-archs", str(path)], check=True, capture_output=True, text=True)
110+
except FileNotFoundError as exc:
111+
raise BuildError(f"architecture check for {path.name}: lipo was not found") from exc
112+
except subprocess.CalledProcessError as exc:
113+
raise BuildError(f"architecture check for {path.name}: lipo failed with exit status {exc.returncode}") from exc
114+
architectures = parse_architectures(result.stdout)
115+
if architectures != (expected,):
116+
raise BuildError(f"architecture check for {path.name}: built {architectures or 'none'}, expected {expected}")
117+
print(f"Verified {path.name}: {expected}")
118+
119+
120+
def _build_bob(library: NativeLibrary, theories_dir: Path, verbose: bool) -> None:
121+
source_dir = library.source_path(theories_dir)
122+
_run(["make", "-f", "makefile_for_lib", "clean"], source_dir, library, verbose)
123+
_run(["make", "-f", "makefile_for_lib"], source_dir, library, verbose)
124+
built = source_dir / "bob2p5_lib.so"
125+
if not built.is_file():
126+
raise BuildError(f"{library.name}: make completed but did not produce {built} (source: {source_dir})")
127+
shutil.copy2(built, library.output_path(theories_dir))
128+
129+
130+
def build_library(library: NativeLibrary, theories_dir: Path, expected: str, verbose: bool) -> None:
131+
print(f"Building {library.name}...")
132+
if library.special_build == "bob_makefile":
133+
_build_bob(library, theories_dir, verbose)
134+
else:
135+
_run(direct_command(library), library.source_path(theories_dir), library, verbose)
136+
output = library.output_path(theories_dir)
137+
if not output.is_file():
138+
raise BuildError(f"{library.name}: expected output was not created: {output}")
139+
verify_architecture(output, expected)
140+
print(f"Built {output.name}")
141+
142+
143+
def clean_outputs(theories_dir: Path) -> None:
144+
for library in NATIVE_LIBRARIES:
145+
output = library.output_path(theories_dir)
146+
if output.exists():
147+
output.unlink()
148+
print(f"Removed {output.name}")
149+
bob_dir = theories_dir / "modified_bob2.5/code/src/obj"
150+
bob_output = bob_dir / "bob2p5_lib.so"
151+
if bob_output.exists():
152+
bob_output.unlink()
153+
print(f"Removed {bob_output}")
154+
for object_file in bob_dir.glob("*.o"):
155+
object_file.unlink()
156+
print(f"Removed {object_file}")
157+
158+
159+
def _parser() -> argparse.ArgumentParser:
160+
return argparse.ArgumentParser(description=__doc__)
161+
162+
163+
def main(argv: Sequence[str] | None = None) -> int:
164+
parser = _parser()
165+
parser.add_argument("--all", action="store_true", help="build all libraries for the current implemented platform")
166+
parser.add_argument("--library", choices=[library.name for library in NATIVE_LIBRARIES], help="build/check one library")
167+
parser.add_argument("--clean", action="store_true", help="remove Darwin outputs and Bob build products")
168+
parser.add_argument("--check", action="store_true", help="check existing Darwin outputs without rebuilding")
169+
parser.add_argument("--verbose", action="store_true", help="print compiler commands")
170+
args = parser.parse_args(argv)
171+
172+
current_platform = detect_platform()
173+
if current_platform != "darwin":
174+
print(f"Platform: {current_platform}")
175+
print("Native-library builds for this platform are not implemented in this first pass.", file=sys.stderr)
176+
return 2
177+
178+
theories_dir = Path(__file__).resolve().parents[1] / "RepTate" / "theories"
179+
target = os.environ.get("MACOSX_DEPLOYMENT_TARGET", "")
180+
print(f"Platform: macOS\nArchitecture: {expected_architecture()}\nMACOSX_DEPLOYMENT_TARGET: {target or '(not set)'}")
181+
selected = [library for library in NATIVE_LIBRARIES if args.library is None or library.name == args.library]
182+
if args.clean:
183+
clean_outputs(theories_dir)
184+
return 0
185+
expected = expected_architecture()
186+
if args.check:
187+
for library in selected:
188+
output = library.output_path(theories_dir)
189+
if not output.is_file():
190+
raise BuildError(f"{library.name}: expected output is missing: {output}")
191+
verify_architecture(output, expected)
192+
return 0
193+
for library in selected:
194+
build_library(library, theories_dir, expected, args.verbose)
195+
return 0
196+
197+
198+
if __name__ == "__main__":
199+
try:
200+
raise SystemExit(main())
201+
except BuildError as exc:
202+
print(f"ERROR: {exc}", file=sys.stderr)
203+
raise SystemExit(1)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import importlib.util
2+
import sys
3+
from pathlib import Path
4+
5+
6+
SCRIPT = Path(__file__).parents[1] / "scripts" / "build_native_libraries.py"
7+
SPEC = importlib.util.spec_from_file_location("build_native_libraries", SCRIPT)
8+
assert SPEC and SPEC.loader
9+
build_native_libraries = importlib.util.module_from_spec(SPEC)
10+
sys.modules[SPEC.name] = build_native_libraries
11+
SPEC.loader.exec_module(build_native_libraries)
12+
13+
14+
def test_platform_detection():
15+
assert build_native_libraries.detect_platform("darwin") == "darwin"
16+
assert build_native_libraries.detect_platform("linux") == "linux"
17+
assert build_native_libraries.detect_platform("win32") == "windows"
18+
19+
20+
def test_inventory_and_outputs():
21+
names = {library.name for library in build_native_libraries.NATIVE_LIBRARIES}
22+
outputs = {library.output_name for library in build_native_libraries.NATIVE_LIBRARIES}
23+
assert names == {"bob", "dtd", "kww", "landscape", "react", "rouse", "rp_blend", "sccr", "schwarzl"}
24+
assert "bob2p5_lib_darwin.so" in outputs
25+
assert len(outputs) == len(names)
26+
27+
28+
def test_command_construction():
29+
library = next(item for item in build_native_libraries.NATIVE_LIBRARIES if item.name == "rouse")
30+
assert build_native_libraries.direct_command(library) == [
31+
"cc", "-dynamiclib", "-fPIC", "-O2", "rouse.c", "-o", "../rouse_lib_darwin.so"
32+
]
33+
landscape = next(item for item in build_native_libraries.NATIVE_LIBRARIES if item.name == "landscape")
34+
assert "-I./" in build_native_libraries.direct_command(landscape)
35+
36+
37+
def test_architecture_parsing():
38+
assert build_native_libraries.parse_architectures("arm64\n") == ("arm64",)
39+
assert build_native_libraries.parse_architectures("arm64 x86_64\n") == ("arm64", "x86_64")

0 commit comments

Comments
 (0)