"""Build the cmpp preprocessor and compile XSPICE .cm code model libraries.""" import subprocess import os import sys import shutil MINGW_BIN = r"C:\mingw64\bin" NGSPICE_ROOT = r"C:\git\ngspice" CMPP_SRC = os.path.join(NGSPICE_ROOT, "src", "xspice", "cmpp") ICM_DIR = os.path.join(NGSPICE_ROOT, "src", "xspice", "icm") INCLUDE_DIR = os.path.join(NGSPICE_ROOT, "src", "include") CONFIG_H_DIR = os.path.join(NGSPICE_ROOT, "visualc", "src", "include") BUILD_DIR = os.path.join(NGSPICE_ROOT, "build", "cmpp_build") CM_OUTPUT_DIR = os.path.join(NGSPICE_ROOT, "pyngspice", "codemodels") CMPP_EXE = os.path.join(BUILD_DIR, "cmpp.exe") def get_env(): env = os.environ.copy() env["PATH"] = MINGW_BIN + ";" + env["PATH"] return env def run(cmd, cwd=None): print(f" > {' '.join(cmd)}") r = subprocess.run(cmd, capture_output=True, text=True, cwd=cwd, env=get_env()) if r.stdout.strip(): for line in r.stdout.strip().split('\n')[:20]: print(f" {line}") if r.stderr.strip(): lines = r.stderr.strip().split('\n') # Show first 10 and last 30 lines of errors if len(lines) > 40: for line in lines[:5]: print(f" {line}") print(f" ... ({len(lines) - 35} lines omitted) ...") for line in lines[-30:]: print(f" {line}") else: for line in lines: print(f" {line}") if r.returncode != 0: print(f" FAILED (rc={r.returncode})") return False return True def build_cmpp(): """Build the cmpp preprocessor tool.""" print("=== Step 1: Building cmpp preprocessor ===") os.makedirs(BUILD_DIR, exist_ok=True) if os.path.exists(CMPP_EXE): print(f" cmpp.exe already exists, skipping") return True cmpp_sources = [ "main.c", "file_buffer.c", "pp_ifs.c", "pp_lst.c", "pp_mod.c", "read_ifs.c", "util.c", "writ_ifs.c", "ifs_yacc.c", "mod_yacc.c", "ifs_lex.c", "mod_lex.c" ] cmd = [ "gcc", "-o", CMPP_EXE, ] + [os.path.join(CMPP_SRC, f) for f in cmpp_sources] + [ f"-I{CMPP_SRC}", f"-I{INCLUDE_DIR}", "-lshlwapi" ] if not run(cmd): return False print(f" OK: {CMPP_EXE}") return True def preprocess_category(category): """Run cmpp on a code model category (analog, digital, etc.).""" print(f"\n=== Step 2: Preprocessing '{category}' code models ===") src_dir = os.path.join(ICM_DIR, category) out_dir = os.path.join(BUILD_DIR, category) # Copy the category directory to build dir for cmpp to work in if os.path.exists(out_dir): shutil.rmtree(out_dir) shutil.copytree(src_dir, out_dir) # Step 2a: Generate registration headers from modpath.lst print(f" Generating registration headers...") env = get_env() env["CMPP_IDIR"] = src_dir env["CMPP_ODIR"] = out_dir r = subprocess.run([CMPP_EXE, "-lst"], capture_output=True, text=True, cwd=out_dir, env=env) if r.returncode != 0: print(f" cmpp -lst FAILED: {r.stderr}") return None print(f" OK: cminfo.h, cmextrn.h generated") # Step 2b: Read modpath.lst to get model names modpath = os.path.join(src_dir, "modpath.lst") with open(modpath) as f: models = [line.strip() for line in f if line.strip()] # Step 2c: Preprocess each model's .ifs and .mod files for model in models: model_src = os.path.join(src_dir, model) model_out = os.path.join(out_dir, model) if not os.path.isdir(model_src): print(f" SKIP: {model} (directory not found)") continue # Preprocess ifspec.ifs -> ifspec.c env_mod = get_env() env_mod["CMPP_IDIR"] = model_src env_mod["CMPP_ODIR"] = model_out r = subprocess.run([CMPP_EXE, "-ifs"], capture_output=True, text=True, cwd=model_out, env=env_mod) if r.returncode != 0: print(f" FAIL: {model}/ifspec.ifs: {r.stderr}") return None # Preprocess cfunc.mod -> cfunc.c r = subprocess.run([CMPP_EXE, "-mod"], capture_output=True, text=True, cwd=model_out, env=env_mod) if r.returncode != 0: print(f" FAIL: {model}/cfunc.mod: {r.stderr}") return None print(f" OK: {len(models)} models preprocessed") return models # Category-specific extra source files and include paths CATEGORY_EXTRAS = { "tlines": { "extra_sources": [ os.path.join(NGSPICE_ROOT, "src", "xspice", "tlines", "tline_common.c"), os.path.join(NGSPICE_ROOT, "src", "xspice", "tlines", "msline_common.c"), ], "extra_includes": [ os.path.join(NGSPICE_ROOT, "src", "xspice", "tlines"), ], }, } def compile_cm(category, models): """Compile preprocessed code models into a .cm DLL.""" print(f"\n=== Step 3: Compiling '{category}.cm' DLL ===") src_dir = os.path.join(ICM_DIR, category) out_dir = os.path.join(BUILD_DIR, category) os.makedirs(CM_OUTPUT_DIR, exist_ok=True) # Collect all .c files to compile c_files = [] # dlmain.c (the DLL entry point with exports) dlmain = os.path.join(ICM_DIR, "dlmain.c") c_files.append(dlmain) # Each model's cfunc.c and ifspec.c for model in models: model_dir = os.path.join(out_dir, model) cfunc_c = os.path.join(model_dir, "cfunc.c") ifspec_c = os.path.join(model_dir, "ifspec.c") if os.path.exists(cfunc_c) and os.path.exists(ifspec_c): c_files.append(cfunc_c) c_files.append(ifspec_c) else: print(f" SKIP: {model} (generated .c files missing)") # UDN (user-defined node) types — udnfunc.c files from udnpath.lst udnpath = os.path.join(src_dir, "udnpath.lst") if os.path.exists(udnpath): with open(udnpath) as f: udns = [line.strip() for line in f if line.strip()] for udn in udns: udnfunc = os.path.join(src_dir, udn, "udnfunc.c") if os.path.exists(udnfunc): c_files.append(udnfunc) print(f" UDN: {udn}/udnfunc.c") # Also need dstring.c for some models that use DS_CREATE dstring_c = os.path.join(NGSPICE_ROOT, "src", "misc", "dstring.c") if os.path.exists(dstring_c): c_files.append(dstring_c) # Category-specific extra sources extras = CATEGORY_EXTRAS.get(category, {}) for src in extras.get("extra_sources", []): if os.path.exists(src): c_files.append(src) print(f" EXTRA: {os.path.basename(src)}") cm_file = os.path.join(CM_OUTPUT_DIR, f"{category}.cm") cmd = [ "gcc", "-shared", "-o", cm_file, ] + c_files + [ f"-I{INCLUDE_DIR}", f"-I{CONFIG_H_DIR}", f"-I{out_dir}", # For cminfo.h, cmextrn.h f"-I{ICM_DIR}", # For dlmain.c includes ] # Category-specific extra include paths for inc in extras.get("extra_includes", []): cmd.append(f"-I{inc}") cmd += [ "-DHAS_PROGREP", "-DXSPICE", "-DSIMULATOR", "-DNG_SHARED_BUILD", "-DNGSPICEDLL", "-Wall", "-Wno-unused-variable", "-Wno-unused-function", "-Wno-sign-compare", "-Wno-maybe-uninitialized", "-Wno-implicit-function-declaration", # GCC 15 makes this an error; some models missing stdlib.h ] if not run(cmd): return False size_kb = os.path.getsize(cm_file) // 1024 print(f" OK: {cm_file} ({size_kb} KB)") return True def build_category(category): """Full pipeline: preprocess + compile a code model category.""" models = preprocess_category(category) if models is None: return False return compile_cm(category, models) if __name__ == "__main__": categories = sys.argv[1:] if len(sys.argv) > 1 else ["analog"] if not build_cmpp(): sys.exit(1) for cat in categories: if not build_category(cat): print(f"\nFailed to build {cat}.cm") sys.exit(1) print(f"\n=== Done! Code models in: {CM_OUTPUT_DIR} ===")