#!/usr/bin/env python """ Build script for pyngspice using MinGW-w64. Single-command build that handles PATH setup, editable install, and optional venv synchronization for pyTesla integration. Usage: python build_mingw.py # Build and install (editable) python build_mingw.py --clean # Clean build directory first python build_mingw.py --sync # Sync .pth files to other venvs python build_mingw.py --verify # Just verify the install works python build_mingw.py --debug # Build with debug logging Run from cmd.exe, NOT Git Bash (to avoid MSYS path mangling). """ import argparse import glob import os import shutil import subprocess import sys from pathlib import Path # ============================================================================ # Configuration # ============================================================================ REPO_ROOT = Path(__file__).parent.resolve() MINGW_BIN = r"C:\mingw64\bin" CMAKE_BIN = r"C:\Program Files\CMake\bin" PACKAGE_DIR = REPO_ROOT / "pyngspice" BUILD_DIR = REPO_ROOT / "build" # Other venvs that need access to pyngspice (e.g., pyTesla's venv) # Add paths here as needed: SYNC_VENVS = [ # r"C:\git\pytesla\.venv", ] def get_clean_path(): """Build a clean PATH that avoids Git MSYS conflicts.""" python_dir = Path(sys.executable).parent python_scripts = python_dir / "Scripts" path_parts = [ str(MINGW_BIN), str(python_dir), str(python_scripts), str(CMAKE_BIN), r"C:\Windows\System32", r"C:\Windows", ] return ";".join(p for p in path_parts if os.path.isdir(p)) def get_build_env(**extra): """Get environment variables for the build.""" env = os.environ.copy() env["PATH"] = get_clean_path() env["CC"] = "gcc" env["CXX"] = "g++" env.update(extra) return env def clean(): """Remove build directory and old .pyd files.""" if BUILD_DIR.exists(): print(f"Removing {BUILD_DIR}...") shutil.rmtree(BUILD_DIR, ignore_errors=True) # Remove old .pyd files from package directory for pyd in PACKAGE_DIR.glob("*.pyd"): print(f"Removing {pyd}...") pyd.unlink() # Uninstall any previous install subprocess.run( [sys.executable, "-m", "pip", "uninstall", "-y", "pyngspice"], capture_output=True, ) print("Clean complete.") def build(debug=False): """Build and install pyngspice in editable mode.""" env = get_build_env() # Verify MinGW is available gcc = shutil.which("gcc", path=env["PATH"]) if not gcc: print(f"ERROR: gcc not found. Ensure MinGW is installed at {MINGW_BIN}") sys.exit(1) result = subprocess.run(["gcc", "--version"], capture_output=True, text=True, env=env) print(f"Using GCC: {result.stdout.splitlines()[0]}") # Build command cmd = [ sys.executable, "-m", "pip", "install", "--no-cache-dir", "-e", ".", ] if debug: # Pass debug flag via environment (picked up by scikit-build-core) env["CMAKE_ARGS"] = "-DCMAKE_BUILD_TYPE=Debug" print(f"\nBuilding pyngspice...") print(f" Command: {' '.join(cmd)}") print(f" Working dir: {REPO_ROOT}") print() result = subprocess.run(cmd, cwd=str(REPO_ROOT), env=env) if result.returncode != 0: print("\nBuild FAILED.") sys.exit(result.returncode) print("\nBuild complete!") def sync_venvs(): """Write .pth files to other venvs so they can import pyngspice.""" if not SYNC_VENVS: print("No venvs configured for sync. Edit SYNC_VENVS in build_mingw.py.") return for venv_path in SYNC_VENVS: venv = Path(venv_path) if not venv.exists(): print(f" SKIP (not found): {venv}") continue # Find site-packages if sys.platform == "win32": site_packages = venv / "Lib" / "site-packages" else: py_ver = f"python{sys.version_info.major}.{sys.version_info.minor}" site_packages = venv / "lib" / py_ver / "site-packages" if not site_packages.exists(): print(f" SKIP (no site-packages): {venv}") continue pth_file = site_packages / "pyngspice.pth" pth_file.write_text(str(REPO_ROOT) + "\n") print(f" Synced: {pth_file}") print("Venv sync complete.") def verify(): """Verify the pyngspice installation works.""" print("\nVerifying pyngspice installation...") # Test basic import result = subprocess.run( [sys.executable, "-c", ( "from pyngspice import NgspiceRunner, _cpp_available; " "print(f' C++ extension: {_cpp_available}'); " "print(f' NgspiceRunner available: {NgspiceRunner.detect()}'); " "from pyngspice import __version__; " "print(f' Version: {__version__}'); " "print(' OK!')" )], cwd=str(REPO_ROOT), ) if result.returncode != 0: print("\nVerification FAILED.") return False return True def main(): parser = argparse.ArgumentParser(description="Build pyngspice with MinGW-w64") parser.add_argument("--clean", action="store_true", help="Clean build artifacts first") parser.add_argument("--sync", action="store_true", help="Sync .pth files to other venvs") parser.add_argument("--verify", action="store_true", help="Verify installation only") parser.add_argument("--debug", action="store_true", help="Build with debug configuration") parser.add_argument("--no-verify", action="store_true", help="Skip post-build verification") args = parser.parse_args() if args.verify: verify() return if args.clean: clean() if args.sync: sync_venvs() return build(debug=args.debug) if not args.no_verify: verify() if SYNC_VENVS: sync_venvs() if __name__ == "__main__": main()