Skip to content

Commit b972640

Browse files
authored
Merge pull request #58 from zasexton/main
mmg platform/machine packaging
2 parents 0707940 + 5ee6d9d commit b972640

19 files changed

Lines changed: 962 additions & 307 deletions

File tree

.github/scripts/basic_smoke_test.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,6 @@
2121
# Never prompt for telemetry consent in CI.
2222
os.environ.setdefault("SVV_TELEMETRY_DISABLED", "1")
2323

24-
import pyvista as pv
25-
26-
from svv.domain.domain import Domain
27-
from svv.forest.forest import Forest
28-
from svv.tree.tree import Tree
29-
from svv.simulation.simulation import Simulation
30-
from svv.utils.remeshing.remesh import remesh_surface
31-
3224

3325
def _log(msg: str) -> None:
3426
print(msg, flush=True)
@@ -118,7 +110,25 @@ def _smoke_gui(timeout_s: int = 45) -> None:
118110

119111

120112
def main() -> None:
113+
import pyvista as pv
114+
115+
from svv.domain.domain import Domain
116+
from svv.forest.forest import Forest
117+
from svv.tree.tree import Tree
118+
from svv.simulation.simulation import Simulation
119+
from svv.utils.remeshing.mmg import get_mmg_candidates, get_mmg_exe
120+
from svv.utils.remeshing.remesh import remesh_surface
121+
121122
_log("SMOKE: starting")
123+
124+
# Log MMG selection candidates early so platform/arch mismatches are obvious.
125+
for tool in ("mmg2d", "mmg3d", "mmgs"):
126+
sel = get_mmg_candidates(tool)
127+
_log(f"SMOKE: {tool}: os={sel.os_dir} arch={sel.arch}")
128+
for p in sel.candidates:
129+
_log(f"SMOKE: {tool}: candidate: {p}")
130+
_log(f"SMOKE: {tool}: selected: {get_mmg_exe(tool)}")
131+
122132
# Domain build (geometry + implicit function + tetrahedral mesh)
123133
_log("SMOKE: domain: create/solve/build")
124134
cube = Domain(pv.Cube().triangulate())

.github/scripts/build_mmg.py

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import argparse
2+
import os
3+
import platform
4+
import shutil
5+
import stat
6+
import subprocess
7+
import tarfile
8+
import tempfile
9+
from pathlib import Path
10+
from typing import Optional
11+
from urllib.request import urlretrieve
12+
13+
14+
MMG_VERSION_DEFAULT = "5.8.0"
15+
MMG_BASENAMES = ("mmg2d_O3", "mmg3d_O3", "mmgs_O3")
16+
17+
18+
def _norm_os(os_name: str) -> str:
19+
if os_name.lower() in {"linux"}:
20+
return "Linux"
21+
if os_name.lower() in {"darwin", "mac", "macos"}:
22+
return "Mac"
23+
if os_name.lower() in {"windows", "win"}:
24+
return "Windows"
25+
raise ValueError(f"Unsupported OS: {os_name}")
26+
27+
28+
def _norm_arch(arch: str) -> str:
29+
a = arch.strip().lower()
30+
if a in {"x86_64", "amd64"}:
31+
return "x86_64"
32+
if a in {"aarch64", "arm64"}:
33+
return "aarch64"
34+
if a in {"universal2"}:
35+
return "universal2"
36+
raise ValueError(f"Unsupported arch: {arch}")
37+
38+
39+
def _tar_safe_extract(t: tarfile.TarFile, dest: Path) -> None:
40+
dest = dest.resolve()
41+
for member in t.getmembers():
42+
member_path = (dest / member.name).resolve()
43+
if not str(member_path).startswith(str(dest) + os.sep):
44+
raise RuntimeError(f"Unsafe tar path: {member.name}")
45+
t.extractall(dest)
46+
47+
48+
def _chmod_x(path: Path) -> None:
49+
if os.name == "nt":
50+
return
51+
try:
52+
mode = path.stat().st_mode
53+
path.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
54+
except Exception:
55+
pass
56+
57+
58+
def _run(cmd: list[str], *, cwd: Optional[Path] = None) -> None:
59+
print("+", " ".join(cmd), flush=True)
60+
subprocess.check_call(cmd, cwd=str(cwd) if cwd else None)
61+
62+
63+
def _find_built_mmg_exes(prefix: Path) -> dict[str, Path]:
64+
found: dict[str, Path] = {}
65+
for p in prefix.rglob("*"):
66+
if not p.is_file():
67+
continue
68+
name = p.name
69+
if os.name == "nt":
70+
if not name.lower().endswith(".exe"):
71+
continue
72+
stem = p.stem
73+
if stem in MMG_BASENAMES:
74+
found[stem] = p
75+
else:
76+
if name in MMG_BASENAMES:
77+
found[name] = p
78+
return found
79+
80+
81+
def build_mmg(*, version: str, os_name: str, arch: str, out_dir: Path, jobs: int) -> None:
82+
os_name = _norm_os(os_name)
83+
arch = _norm_arch(arch)
84+
out_dir.mkdir(parents=True, exist_ok=True)
85+
86+
url = f"https://github.com/MmgTools/mmg/archive/refs/tags/v{version}.tar.gz"
87+
88+
with tempfile.TemporaryDirectory() as td:
89+
tmp = Path(td)
90+
tar_path = tmp / "mmg.tar.gz"
91+
src_root = tmp / "src"
92+
build_dir = tmp / "build"
93+
install_dir = tmp / "install"
94+
src_root.mkdir(parents=True, exist_ok=True)
95+
build_dir.mkdir(parents=True, exist_ok=True)
96+
install_dir.mkdir(parents=True, exist_ok=True)
97+
98+
print(f"Downloading MMG v{version}...", flush=True)
99+
urlretrieve(url, tar_path) # nosec - trusted upstream in CI/release flows
100+
101+
print("Extracting MMG...", flush=True)
102+
with tarfile.open(tar_path, "r:gz") as t:
103+
_tar_safe_extract(t, src_root)
104+
105+
# Archive extracts into mmg-<ver>/
106+
subdirs = [p for p in src_root.iterdir() if p.is_dir()]
107+
if not subdirs:
108+
raise RuntimeError("MMG source extraction produced no subdirectory")
109+
mmg_src = subdirs[0]
110+
111+
cmake_cmd = [
112+
"cmake",
113+
"-S",
114+
str(mmg_src),
115+
"-B",
116+
str(build_dir),
117+
"-DCMAKE_BUILD_TYPE=Release",
118+
f"-DCMAKE_INSTALL_PREFIX={install_dir}",
119+
"-DCMAKE_C_FLAGS_RELEASE=-O3 -DNDEBUG",
120+
]
121+
122+
if os_name == "Mac" and arch == "universal2":
123+
# Build fat binaries.
124+
deployment = os.environ.get("MACOSX_DEPLOYMENT_TARGET", "11.0").strip()
125+
cmake_cmd += [
126+
"-DCMAKE_OSX_ARCHITECTURES=x86_64;arm64",
127+
f"-DCMAKE_OSX_DEPLOYMENT_TARGET={deployment}",
128+
]
129+
130+
if os_name == "Windows":
131+
# Prefer a 64-bit build when using Visual Studio generators.
132+
cmake_cmd += ["-A", "x64"]
133+
134+
_run(cmake_cmd)
135+
136+
build_cmd = ["cmake", "--build", str(build_dir), "--parallel", str(max(1, jobs))]
137+
if os_name == "Windows":
138+
build_cmd += ["--config", "Release"]
139+
_run(build_cmd)
140+
141+
install_cmd = ["cmake", "--install", str(build_dir)]
142+
if os_name == "Windows":
143+
install_cmd += ["--config", "Release"]
144+
_run(install_cmd)
145+
146+
found = _find_built_mmg_exes(install_dir)
147+
missing = sorted(set(MMG_BASENAMES) - set(found.keys()))
148+
if missing:
149+
raise RuntimeError(f"MMG build succeeded but executables not found: {missing}")
150+
151+
# Copy into the repo/package layout
152+
for stem in MMG_BASENAMES:
153+
src = found[stem]
154+
dest = out_dir / src.name
155+
if dest.exists():
156+
dest.unlink()
157+
shutil.copy2(src, dest)
158+
_chmod_x(dest)
159+
160+
print("MMG installed to:", out_dir, flush=True)
161+
for stem in MMG_BASENAMES:
162+
exe = out_dir / (stem + (".exe" if os.name == "nt" else ""))
163+
if exe.exists():
164+
print(" -", exe, flush=True)
165+
166+
167+
def main() -> None:
168+
parser = argparse.ArgumentParser(description="Build and stage MMG executables for svv packaging.")
169+
parser.add_argument("--version", default=MMG_VERSION_DEFAULT, help="MMG version (default: 5.8.0)")
170+
parser.add_argument("--os", dest="os_name", default=platform.system(), help="Target OS (Linux/Mac/Windows)")
171+
parser.add_argument("--arch", default=platform.machine(), help="Target arch (x86_64/aarch64/universal2)")
172+
parser.add_argument(
173+
"--out-dir",
174+
default=None,
175+
help="Output directory for executables (default: svv/utils/remeshing/<OS>/<arch>)",
176+
)
177+
parser.add_argument("--jobs", type=int, default=(os.cpu_count() or 1), help="Parallel build jobs")
178+
args = parser.parse_args()
179+
180+
os_name = _norm_os(args.os_name)
181+
arch = _norm_arch(args.arch)
182+
183+
repo_root = Path(__file__).resolve().parents[2]
184+
default_out = repo_root / "svv" / "utils" / "remeshing" / os_name / arch
185+
out_dir = Path(args.out_dir).resolve() if args.out_dir else default_out
186+
187+
build_mmg(version=args.version, os_name=os_name, arch=arch, out_dir=out_dir, jobs=args.jobs)
188+
189+
190+
if __name__ == "__main__":
191+
main()

.github/workflows/basic-smoke-test.yml

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ jobs:
3434
run: |
3535
sudo apt-get update
3636
sudo apt-get install -y --no-install-recommends \
37+
xvfb \
3738
libegl1 \
3839
libgl1 \
3940
libx11-xcb1 \
@@ -56,36 +57,52 @@ jobs:
5657
python -m pip install -r requirements.txt
5758
python -m pip install cmake
5859
59-
- name: Build svVascularize
60+
- name: Build MMG (v5.8.0, Release, -O3)
61+
run: |
62+
if [ "${{ runner.os }}" = "macOS" ]; then
63+
python .github/scripts/build_mmg.py --version 5.8.0 --arch universal2 --jobs 2
64+
elif [ "${{ runner.os }}" = "Windows" ]; then
65+
python .github/scripts/build_mmg.py --version 5.8.0 --arch x86_64 --jobs 2
66+
else
67+
python .github/scripts/build_mmg.py --version 5.8.0 --arch x86_64 --jobs 2
68+
fi
69+
70+
- name: Install svVascularize
6071
env:
61-
SVV_BUILD_MMG: "1"
72+
SVV_REQUIRE_MMG: "1"
73+
SVV_MMG_ARCH: ${{ runner.os == 'macOS' && 'universal2' || '' }}
6274
run: |
63-
# Disable build isolation so build-time tools (cmake, compilers) installed above
64-
# are visible to setup.py when building MMG.
65-
pip install . --no-build-isolation
75+
python -m pip install .
6676
6777
- name: Verify MMG executables are installed
6878
run: |
6979
python - << 'PY'
7080
import os
71-
import sys
72-
import svv
81+
import subprocess
82+
import tempfile
7383
74-
bin_dir = os.path.join(os.path.dirname(os.path.abspath(svv.__file__)), "bin")
75-
print("svv/bin:", bin_dir)
76-
if not os.path.isdir(bin_dir):
77-
raise SystemExit("svv/bin directory missing; MMG did not build/package correctly.")
84+
# Ensure we validate the *installed* package, not the repo checkout
85+
# (stdin execution puts the current working directory on sys.path).
86+
os.chdir(tempfile.mkdtemp(prefix="svv-mmg-verify-"))
7887
79-
names = set(os.listdir(bin_dir))
80-
expected = {"mmg2d_O3", "mmg3d_O3", "mmgs_O3"}
81-
if os.name == "nt":
82-
expected = {e + ".exe" for e in expected}
88+
from svv.utils.remeshing.mmg import get_mmg_exe
8389
84-
missing = sorted(expected - names)
85-
if missing:
86-
raise SystemExit(f"Missing MMG executables in svv/bin: {missing}. Found: {sorted(names)}")
90+
for tool in ("mmg2d", "mmg3d", "mmgs"):
91+
exe = get_mmg_exe(tool)
92+
print(f"{tool} -> {exe}", flush=True)
93+
if os.name != "nt" and not os.access(exe, os.X_OK):
94+
raise SystemExit(f"{exe} is not marked executable")
8795
88-
print("MMG executables present:", sorted(expected))
96+
# MMG tools often return a non-zero code for help/usage (commonly 1 or 2),
97+
# so don't treat that as a failure; we only want to ensure the binary runs.
98+
proc = subprocess.run(
99+
[str(exe), "-h"],
100+
stdout=subprocess.DEVNULL,
101+
stderr=subprocess.DEVNULL,
102+
timeout=10,
103+
check=False,
104+
)
105+
print(f"{tool} -h exit: {proc.returncode}", flush=True)
89106
PY
90107
91108
- name: Run basic smoke test
@@ -97,6 +114,7 @@ jobs:
97114
fi
98115
env:
99116
SVV_TELEMETRY_DISABLED: "1"
117+
SVV_GUI_DISABLE_VTK: "1"
100118
SVV_GUI_GL_MODE: "software"
101119
QT_X11_NO_MITSHM: "1"
102120
SVV_USEARCH_THREADS: "1"

0 commit comments

Comments
 (0)