diff --git a/.github/workflows/build-switchgentool.yml b/.github/workflows/build-switchgentool.yml index 6458d95..99ffdca 100644 --- a/.github/workflows/build-switchgentool.yml +++ b/.github/workflows/build-switchgentool.yml @@ -63,7 +63,7 @@ jobs: echo "jinja2>=3.0.0" > requirements.txt fi pip install -r requirements.txt - pip install "pyinstaller==6.10.*" + pip install "pyinstaller==6.11.*" - name: Quick source test (run as script) shell: bash @@ -75,14 +75,18 @@ jobs: if: matrix.os == 'windows-latest' shell: powershell run: | - pyinstaller --onefile --clean --noconfirm ` + pyinstaller --onefile --clean --noconfirm --noupx ` --name network_config_generator ` + --additional-hooks-dir=. ` --add-data "input;input" ` --collect-all jinja2 ` --hidden-import=convertors ` --hidden-import=convertors.convertors_lab_switch_json ` --hidden-import=convertors.convertors_bmc_switch_json ` --collect-submodules=convertors ` + --exclude-module tkinter ` + --exclude-module matplotlib ` + --exclude-module PIL ` --paths=src ` src/main.py if (!(Test-Path "dist\\${{ matrix.exe_name }}")) { Write-Error "Build failed"; exit 1 } @@ -93,12 +97,16 @@ jobs: run: | pyinstaller --onefile --clean --noconfirm \ --name network_config_generator \ + --additional-hooks-dir=. \ --add-data "input:input" \ --collect-all jinja2 \ --hidden-import=convertors \ --hidden-import=convertors.convertors_lab_switch_json \ --hidden-import=convertors.convertors_bmc_switch_json \ --collect-submodules=convertors \ + --exclude-module tkinter \ + --exclude-module matplotlib \ + --exclude-module PIL \ --paths=src \ src/main.py test -f "dist/${{ matrix.exe_name }}" || (echo "Build failed" && exit 1) @@ -109,6 +117,20 @@ jobs: run: | chmod +x "dist/${{ matrix.exe_name }}" || true "dist/${{ matrix.exe_name }}" --help || (echo "Executable failed to run" && exit 1) + + - name: Test BMC converter module inclusion + shell: bash + run: | + python -c " + import sys + sys.path.insert(0, 'src') + try: + from convertors.convertors_bmc_switch_json import convert_bmc_switches + print('[OK] BMC converter module can be imported') + except ImportError as e: + print('[ERROR] BMC converter module import failed:', e) + sys.exit(1) + " - name: Stage artifact shell: bash diff --git a/hook-convertors.py b/hook-convertors.py new file mode 100644 index 0000000..e4f64a6 --- /dev/null +++ b/hook-convertors.py @@ -0,0 +1,13 @@ +# PyInstaller hook for convertors package +# This ensures all convertor modules are included in the bundle + +from PyInstaller.utils.hooks import collect_submodules, collect_data_files + +# Collect all submodules from the convertors package +hiddenimports = collect_submodules('convertors') + +# Also explicitly add the BMC converter module +if 'convertors.convertors_bmc_switch_json' not in hiddenimports: + hiddenimports.append('convertors.convertors_bmc_switch_json') + +print(f"[HOOK] convertors hiddenimports: {hiddenimports}") diff --git a/src/convertors/convertors_bmc_switch_json.py b/src/convertors/convertors_bmc_switch_json.py index f45db11..5a33878 100644 --- a/src/convertors/convertors_bmc_switch_json.py +++ b/src/convertors/convertors_bmc_switch_json.py @@ -234,25 +234,31 @@ def _build_interfaces(self, switch_data: Dict) -> List[Dict]: model = switch_data.get("Model", "").upper() # Load interface template for BMC switch - template_relative = Path("input/switch_interface_templates") / make / f"{model}.json" + template_relative = Path("input") / "switch_interface_templates" / make / f"{model}.json" # Try multiple path resolution strategies import sys if getattr(sys, 'frozen', False): # Running as PyInstaller bundle - use _MEIPASS - template_path = get_real_path(template_relative) + base_path = Path(sys._MEIPASS) + template_path = base_path / template_relative else: # Running as script - try both current directory and parent directory - template_path = get_real_path(template_relative) + template_path = template_relative.resolve() if not template_path.exists(): # Try from parent directory (in case we're running from src/) - template_path = get_real_path(Path("..") / template_relative) + template_path = (Path("..") / template_relative).resolve() if not template_path.exists(): + import sys + debug_info = f"sys.frozen={getattr(sys, 'frozen', False)}, sys._MEIPASS={getattr(sys, '_MEIPASS', 'N/A')}" raise FileNotFoundError( - f"[!] BMC interface template not found: {template_path}\n" + f"[!] BMC interface template not found!\n" f" Looking for model: {model} (make: {make})\n" - f" Searched: {template_relative}" + f" Relative path: {template_relative}\n" + f" Resolved path: {template_path}\n" + f" File exists: {template_path.exists()}\n" + f" Debug: {debug_info}" ) try: diff --git a/src/convertors/convertors_lab_switch_json.py b/src/convertors/convertors_lab_switch_json.py index 04d2e7d..e255bb1 100644 --- a/src/convertors/convertors_lab_switch_json.py +++ b/src/convertors/convertors_lab_switch_json.py @@ -2,11 +2,24 @@ from copy import deepcopy from pathlib import Path from collections import defaultdict + +# IMPORTANT: Unconditional import for PyInstaller detection +# PyInstaller's static analysis needs to see this import at module level without any conditionals +from . import convertors_bmc_switch_json + try: from ..loader import get_real_path # package style except ImportError: from loader import get_real_path # fallback script style +# Import BMC converter function with fallback handling +try: + from .convertors_bmc_switch_json import convert_bmc_switches + _bmc_available = True +except ImportError: + convert_bmc_switches = None + _bmc_available = False + # ── Static config ───────────────────────────────────────────────────────── SWITCH_TYPES = ["TOR1", "TOR2"] TOR1, TOR2 = "TOR1", "TOR2" @@ -542,14 +555,16 @@ def convert_switch_input_json(input_data: dict, output_dir: str = DEFAULT_OUTPUT print(f"[✓] Wrote {out_file}") - # Convert BMC switches - try: - from .convertors_bmc_switch_json import convert_bmc_switches - convert_bmc_switches(input_data, output_dir) - except ImportError as e: - print(f"[!] BMC converter not available: {e}") - except Exception as e: - print(f"[!] Error converting BMC switches: {e}") + # Convert BMC switches using the module-level import + if _bmc_available and convert_bmc_switches: + try: + convert_bmc_switches(input_data, output_dir) + except Exception as e: + print(f"[!] Error converting BMC switches: {e}") + import traceback + traceback.print_exc() + else: + print(f"[!] BMC converter not available - module not imported") def convert_all_switches_json(input_data: dict, output_dir: str = DEFAULT_OUTPUT_DIR): @@ -560,13 +575,7 @@ def convert_all_switches_json(input_data: dict, output_dir: str = DEFAULT_OUTPUT print("[*] Converting ToR switches...") convert_switch_input_json(input_data, output_dir) - # Import and call BMC converter (separate module for clean separation) - try: - from .convertors_bmc_switch_json import convert_bmc_switches - print("[*] Converting BMC switches...") - convert_bmc_switches(input_data, output_dir) - except ImportError as e: - print(f"[!] BMC converter not available: {e}") - + # BMC converter already called within convert_switch_input_json + # No need to call again here to avoid duplicate conversion print("[✓] All switch conversions completed.") diff --git a/src/main.py b/src/main.py index 876edeb..463e8cf 100644 --- a/src/main.py +++ b/src/main.py @@ -16,6 +16,31 @@ from generator import generate_config # type: ignore from loader import get_real_path, load_input_json # type: ignore # Only used for PyInstaller-packed assets +# Configure UTF-8 encoding for Windows console (fixes emoji display issues in executables) +if sys.platform == "win32": + try: + # Try to set console to UTF-8 mode + import os + os.system("chcp 65001 > nul 2>&1") + # Reconfigure stdout/stderr with UTF-8 encoding + if hasattr(sys.stdout, 'reconfigure'): + sys.stdout.reconfigure(encoding='utf-8', errors='replace') + sys.stderr.reconfigure(encoding='utf-8', errors='replace') + except: + pass # If it fails, we'll use safe_print fallback + +def safe_print(text): + """ + Safely print text, handling Unicode characters that might not be supported in console. + Falls back to ASCII-safe alternatives if encoding fails. + """ + try: + print(text) + except UnicodeEncodeError: + # Remove or replace problematic Unicode characters + safe_text = text.encode('ascii', errors='replace').decode('ascii') + print(safe_text) + def load_convertor(convertor_module_path): """ Dynamically load a convertor module and return its convert function. @@ -65,8 +90,8 @@ def convert_to_standard_format(input_file_path, output_dir, convertor_module_pat Convert lab format to standard format JSON files using specified convertor. Returns list of generated standard format files. """ - print("🔄 Converting from lab format to standard format...") - print(f"📦 Using convertor: {convertor_module_path}") + safe_print("🔄 Converting from lab format to standard format...") + safe_print(f"📦 Using convertor: {convertor_module_path}") # Load lab format data data = load_input_json(str(input_file_path)) @@ -86,7 +111,7 @@ def convert_to_standard_format(input_file_path, output_dir, convertor_module_pat if not generated_files: raise RuntimeError("No standard format files were generated during conversion") - print(f"✅ Generated {len(generated_files)} standard format files:") + safe_print(f"✅ Generated {len(generated_files)} standard format files:") for file in generated_files: print(f" - {file}") @@ -124,11 +149,11 @@ def main(): else: template_folder = template_folder_arg.resolve() - print(f"🧾 Input JSON File: {input_json_path}") - print(f"🧩 Template Folder: {template_folder}") - print(f"📁 Output Directory: {output_folder_path}") + safe_print(f"🧾 Input JSON File: {input_json_path}") + safe_print(f"🧩 Template Folder: {template_folder}") + safe_print(f"📁 Output Directory: {output_folder_path}") if args.convertor != parser.get_default('convertor'): - print(f"🔄 Custom Convertor: {args.convertor}") + safe_print(f"🔄 Custom Convertor: {args.convertor}") # === Validation === if not input_json_path.exists(): @@ -142,7 +167,7 @@ def main(): output_folder_path.mkdir(parents=True, exist_ok=True) # === Step 1: Check if input is in standard format === - print("🔍 Checking input format...") + safe_print("🔍 Checking input format...") data = load_input_json(str(input_json_path)) if data is None: print(f"[ERROR] Failed to load input JSON: {input_json_path}") @@ -151,10 +176,10 @@ def main(): standard_format_files = [] if is_standard_format(data): - print("✅ Input is already in standard format") + safe_print("✅ Input is already in standard format") standard_format_files = [input_json_path] else: - print("⚠️ Input is in lab format - conversion required") + safe_print("⚠️ Input is in lab format - conversion required") try: # Create temporary subdirectory for conversion within output folder temp_conversion_subdir = output_folder_path / ".temp_conversion" @@ -167,22 +192,22 @@ def main(): args.convertor ) except Exception as e: - print(f"❌ Failed to convert to standard format: {e}") - print(f"\n💡 Troubleshooting tips:") + safe_print(f"❌ Failed to convert to standard format: {e}") + safe_print(f"\n💡 Troubleshooting tips:") print(f" - Ensure your input file is in the correct format for convertor: {args.convertor}") print(f" - Check if the convertor module exists and has 'convert_switch_input_json' function") print(f" - For custom convertors, use: --convertor your.custom.convertor.module") sys.exit(1) # === Step 2: Generate configs for each standard format file === - print(f"\n🏗️ Generating configs for {len(standard_format_files)} switch(es)...") + safe_print(f"\n🏗️ Generating configs for {len(standard_format_files)} switch(es)...") total_success = 0 total_failed = 0 conversion_used = not is_standard_format(data) for std_file in standard_format_files: - print(f"\n📝 Processing: {std_file.name}") + safe_print(f"\n📝 Processing: {std_file.name}") try: # Create subdirectory for each switch's output @@ -194,7 +219,7 @@ def main(): import shutil std_json_copy = switch_output_dir / f"std_{std_file.name}" shutil.copy2(std_file, std_json_copy) - print(f"📄 Standard JSON saved: {std_json_copy.name}") + safe_print(f"📄 Standard JSON saved: {std_json_copy.name}") generate_config( input_std_json=str(std_file), @@ -202,10 +227,10 @@ def main(): output_folder=str(switch_output_dir) ) total_success += 1 - print(f"✅ Generated configs for {std_file.name} in {switch_output_dir}") + safe_print(f"✅ Generated configs for {std_file.name} in {switch_output_dir}") except Exception as e: - print(f"❌ Failed to generate configs for {std_file.name}: {e}") + safe_print(f"❌ Failed to generate configs for {std_file.name}: {e}") total_failed += 1 # === Cleanup conversion artifacts === @@ -213,23 +238,23 @@ def main(): # Clean up temporary conversion subdirectory temp_conversion_subdir = output_folder_path / ".temp_conversion" if temp_conversion_subdir.exists(): - print(f"\n🧹 Cleaning up temporary conversion directory...") + safe_print(f"\n🧹 Cleaning up temporary conversion directory...") shutil.rmtree(temp_conversion_subdir, ignore_errors=True) # Keep the original converted JSON files in the root directory for user verification - print("� Original converted JSON files kept in output directory for verification") + safe_print("📋 Original converted JSON files kept in output directory for verification") # === Summary === - print(f"\n🎯 Summary:") - print(f" ✅ Successfully processed: {total_success} switch(es)") + safe_print(f"\n🎯 Summary:") + safe_print(f" ✅ Successfully processed: {total_success} switch(es)") if total_failed > 0: - print(f" ❌ Failed to process: {total_failed} switch(es)") - print(f" 📁 Output directory: {output_folder_path}") + safe_print(f" ❌ Failed to process: {total_failed} switch(es)") + safe_print(f" 📁 Output directory: {output_folder_path}") if total_failed > 0: sys.exit(1) else: - print("🎉 All configs generated successfully!") + safe_print("🎉 All configs generated successfully!") if __name__ == "__main__": main()