Skip to content

Commit eb28193

Browse files
hjmjohnsonclaude
andcommitted
ENH: Add scripts to build all ITK + remote module wheels from latest main
Add shell and Python scripts that clone ITK, ITKPythonPackage, and all remote modules from their latest main branches, then build ITK Python wheels followed by every remote module that has Python wrapping. All wheels are collected into a single /tmp/<timestamp>_LatestITKPython/dist directory. Both scripts parse ITK's Modules/Remote/*.remote.cmake files to discover remote module git repositories, filter to those with wrapping/ and pyproject.toml, and build each module wheel reusing the ITK build from the first step. Usage: ./scripts/build-all-latest-wheels.sh [platform-env] python scripts/build_all_latest_wheels.py --platform-env linux-py311 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 221359d commit eb28193

2 files changed

Lines changed: 291 additions & 0 deletions

File tree

scripts/build-all-latest-wheels.sh

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
#!/bin/bash
2+
# Build ITK + all remote module Python wheels from latest main branches.
3+
# Usage: ./scripts/build-all-latest-wheels.sh [platform-env]
4+
# Example: ./scripts/build-all-latest-wheels.sh linux-py311
5+
set -euo pipefail
6+
7+
PLATFORM_ENV="${1:-linux-py311}"
8+
TIMESTAMP=$(date +%Y%m%d%H%M%S)
9+
WORKDIR="/tmp/${TIMESTAMP}_LatestITKPython"
10+
DIST_DIR="${WORKDIR}/dist"
11+
ITK_REPO="https://github.com/InsightSoftwareConsortium/ITK.git"
12+
IPP_REPO="https://github.com/BRAINSia/ITKPythonPackage.git"
13+
IPP_BRANCH="python-build-system"
14+
15+
mkdir -p "${DIST_DIR}"
16+
echo "=== Build directory: ${WORKDIR}"
17+
echo "=== Platform: ${PLATFORM_ENV}"
18+
19+
# 1) Clone ITK
20+
echo "=== Cloning ITK (main)..."
21+
git clone --depth 1 --branch main "${ITK_REPO}" "${WORKDIR}/ITK"
22+
23+
# 2) Clone ITKPythonPackage
24+
echo "=== Cloning ITKPythonPackage (${IPP_BRANCH})..."
25+
git clone --branch "${IPP_BRANCH}" "${IPP_REPO}" "${WORKDIR}/ITKPythonPackage"
26+
27+
# 3) Parse remote modules from ITK and clone each
28+
echo "=== Cloning remote modules..."
29+
MODULES_DIR="${WORKDIR}/modules"
30+
mkdir -p "${MODULES_DIR}"
31+
module_list=()
32+
33+
for rc in "${WORKDIR}"/ITK/Modules/Remote/*.remote.cmake; do
34+
name=$(basename "${rc}" .remote.cmake)
35+
repo=$(grep 'GIT_REPOSITORY' "${rc}" | sed 's/.*GIT_REPOSITORY *//;s/ *)//;s/[[:space:]]*$//')
36+
[ -z "${repo}" ] && continue
37+
38+
echo " Cloning ${name} from ${repo}..."
39+
if git clone --depth 1 "${repo}" "${MODULES_DIR}/${name}" 2>/dev/null; then
40+
# Only keep modules that have Python wrapping
41+
if [ -d "${MODULES_DIR}/${name}/wrapping" ] && [ -f "${MODULES_DIR}/${name}/pyproject.toml" ]; then
42+
module_list+=("${name}")
43+
else
44+
rm -rf "${MODULES_DIR}/${name}"
45+
fi
46+
else
47+
echo " WARNING: Failed to clone ${name}, skipping"
48+
fi
49+
done
50+
51+
echo "=== ${#module_list[@]} modules with Python wrapping"
52+
53+
# 4) Build ITK wheels
54+
cd "${WORKDIR}/ITKPythonPackage"
55+
echo "=== Building ITK Python wheels..."
56+
pixi run -e "${PLATFORM_ENV}" -- python scripts/build_wheels.py \
57+
--platform-env "${PLATFORM_ENV}" \
58+
--itk-git-tag main \
59+
--itk-source-dir "${WORKDIR}/ITK" \
60+
--no-build-itk-tarball-cache \
61+
--no-use-sudo \
62+
--build-dir-root "${WORKDIR}/build"
63+
64+
# Copy ITK wheels to dist
65+
cp "${WORKDIR}"/build/dist/*.whl "${DIST_DIR}/" 2>/dev/null || true
66+
67+
# 5) Build each remote module wheel
68+
failed_modules=()
69+
for name in "${module_list[@]}"; do
70+
echo "=== Building ${name}..."
71+
if pixi run -e "${PLATFORM_ENV}" -- python scripts/build_wheels.py \
72+
--platform-env "${PLATFORM_ENV}" \
73+
--itk-git-tag main \
74+
--itk-source-dir "${WORKDIR}/ITK" \
75+
--module-source-dir "${MODULES_DIR}/${name}" \
76+
--no-build-itk-tarball-cache \
77+
--no-use-sudo \
78+
--skip-itk-build \
79+
--skip-itk-wheel-build \
80+
--build-dir-root "${WORKDIR}/build" 2>&1; then
81+
cp "${MODULES_DIR}/${name}"/dist/*.whl "${DIST_DIR}/" 2>/dev/null || true
82+
else
83+
echo " FAILED: ${name}"
84+
failed_modules+=("${name}")
85+
fi
86+
done
87+
88+
# 6) Summary
89+
echo ""
90+
echo "=== Build complete ==="
91+
echo "Wheels: ${DIST_DIR}"
92+
ls -1 "${DIST_DIR}"/*.whl 2>/dev/null | wc -l
93+
echo "total wheels produced"
94+
95+
if [ ${#failed_modules[@]} -gt 0 ]; then
96+
echo ""
97+
echo "Failed modules (${#failed_modules[@]}):"
98+
printf ' %s\n' "${failed_modules[@]}"
99+
fi

scripts/build_all_latest_wheels.py

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
#!/usr/bin/env python3
2+
"""Build ITK + all remote module Python wheels from latest main branches.
3+
4+
Usage::
5+
6+
python scripts/build_all_latest_wheels.py [--platform-env linux-py311]
7+
python scripts/build_all_latest_wheels.py --help
8+
"""
9+
10+
import argparse
11+
import re
12+
import shutil
13+
import subprocess
14+
import sys
15+
from datetime import datetime
16+
from pathlib import Path
17+
18+
19+
def run(cmd: list[str], **kwargs) -> subprocess.CompletedProcess:
20+
print(f" $ {' '.join(str(c) for c in cmd)}")
21+
return subprocess.run(cmd, check=True, **kwargs)
22+
23+
24+
def clone(repo: str, dest: Path, branch: str | None = None) -> bool:
25+
cmd = ["git", "clone", "--depth", "1"]
26+
if branch:
27+
cmd += ["--branch", branch]
28+
cmd += [repo, str(dest)]
29+
try:
30+
run(cmd, capture_output=True)
31+
return True
32+
except subprocess.CalledProcessError:
33+
return False
34+
35+
36+
def parse_remote_modules(itk_dir: Path) -> list[tuple[str, str]]:
37+
"""Parse ITK remote .cmake files, return [(name, git_url), ...]."""
38+
modules = []
39+
for rc in sorted((itk_dir / "Modules" / "Remote").glob("*.remote.cmake")):
40+
name = rc.stem.replace(".remote", "")
41+
text = rc.read_text()
42+
m = re.search(r"GIT_REPOSITORY\s+(\S+)", text)
43+
if m:
44+
modules.append((name, m.group(1)))
45+
return modules
46+
47+
48+
def build_wheels(
49+
ipp_dir: Path,
50+
platform_env: str,
51+
build_dir: Path,
52+
itk_source: Path,
53+
module_source: Path | None = None,
54+
skip_itk: bool = False,
55+
) -> bool:
56+
cmd = [
57+
"pixi",
58+
"run",
59+
"-e",
60+
platform_env,
61+
"--",
62+
"python",
63+
"scripts/build_wheels.py",
64+
"--platform-env",
65+
platform_env,
66+
"--itk-git-tag",
67+
"main",
68+
"--itk-source-dir",
69+
str(itk_source),
70+
"--no-build-itk-tarball-cache",
71+
"--no-use-sudo",
72+
"--build-dir-root",
73+
str(build_dir),
74+
]
75+
if module_source:
76+
cmd += ["--module-source-dir", str(module_source)]
77+
if skip_itk:
78+
cmd += ["--skip-itk-build", "--skip-itk-wheel-build"]
79+
80+
try:
81+
run(cmd, cwd=ipp_dir)
82+
return True
83+
except subprocess.CalledProcessError:
84+
return False
85+
86+
87+
def main():
88+
parser = argparse.ArgumentParser(description=__doc__)
89+
parser.add_argument(
90+
"--platform-env", default="linux-py311", help="Pixi environment name"
91+
)
92+
parser.add_argument(
93+
"--ipp-branch",
94+
default="python-build-system",
95+
help="ITKPythonPackage branch to use",
96+
)
97+
parser.add_argument(
98+
"--ipp-repo",
99+
default="https://github.com/BRAINSia/ITKPythonPackage.git",
100+
help="ITKPythonPackage git URL",
101+
)
102+
parser.add_argument(
103+
"--itk-repo",
104+
default="https://github.com/InsightSoftwareConsortium/ITK.git",
105+
help="ITK git URL",
106+
)
107+
args = parser.parse_args()
108+
109+
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
110+
workdir = Path(f"/tmp/{timestamp}_LatestITKPython")
111+
dist_dir = workdir / "dist"
112+
dist_dir.mkdir(parents=True)
113+
build_dir = workdir / "build"
114+
115+
print(f"=== Build directory: {workdir}")
116+
print(f"=== Platform: {args.platform_env}")
117+
118+
# 1) Clone ITK
119+
print("=== Cloning ITK (main)...")
120+
itk_dir = workdir / "ITK"
121+
clone(args.itk_repo, itk_dir, branch="main")
122+
123+
# 2) Clone ITKPythonPackage
124+
print(f"=== Cloning ITKPythonPackage ({args.ipp_branch})...")
125+
ipp_dir = workdir / "ITKPythonPackage"
126+
clone(args.ipp_repo, ipp_dir, branch=args.ipp_branch)
127+
128+
# 3) Clone remote modules
129+
print("=== Cloning remote modules...")
130+
modules_dir = workdir / "modules"
131+
modules_dir.mkdir()
132+
remote_modules = parse_remote_modules(itk_dir)
133+
134+
module_list: list[str] = []
135+
for name, repo in remote_modules:
136+
mod_dir = modules_dir / name
137+
if not clone(repo, mod_dir):
138+
print(f" WARNING: Failed to clone {name}, skipping")
139+
continue
140+
# Keep only modules with Python wrapping
141+
if (mod_dir / "wrapping").is_dir() and (mod_dir / "pyproject.toml").is_file():
142+
module_list.append(name)
143+
else:
144+
shutil.rmtree(mod_dir)
145+
146+
print(f"=== {len(module_list)} modules with Python wrapping")
147+
148+
# 4) Build ITK wheels
149+
print("=== Building ITK Python wheels...")
150+
if not build_wheels(ipp_dir, args.platform_env, build_dir, itk_dir):
151+
print("FATAL: ITK wheel build failed")
152+
sys.exit(1)
153+
154+
# Copy ITK wheels to dist
155+
for whl in (build_dir / "dist").glob("*.whl"):
156+
shutil.copy2(whl, dist_dir)
157+
158+
# 5) Build each remote module wheel
159+
failed: list[str] = []
160+
for name in module_list:
161+
print(f"=== Building {name}...")
162+
mod_dir = modules_dir / name
163+
if build_wheels(
164+
ipp_dir,
165+
args.platform_env,
166+
build_dir,
167+
itk_dir,
168+
module_source=mod_dir,
169+
skip_itk=True,
170+
):
171+
for whl in (mod_dir / "dist").glob("*.whl"):
172+
shutil.copy2(whl, dist_dir)
173+
else:
174+
print(f" FAILED: {name}")
175+
failed.append(name)
176+
177+
# 6) Summary
178+
wheels = list(dist_dir.glob("*.whl"))
179+
print()
180+
print("=== Build complete ===")
181+
print(f"Wheels: {dist_dir}")
182+
print(f"{len(wheels)} total wheels produced")
183+
184+
if failed:
185+
print(f"\nFailed modules ({len(failed)}):")
186+
for name in failed:
187+
print(f" {name}")
188+
sys.exit(1)
189+
190+
191+
if __name__ == "__main__":
192+
main()

0 commit comments

Comments
 (0)