#!/usr/bin/env python3 """ Dynamic PLC Compilation Manager =============================== Manages the L5X2ACD compilation directory by: 1. Wiping existing files completely before each run 2. Generating project-specific batch files 3. Copying appropriate L5X files based on the command/project 4. Creating dynamic compilation setups for different projects Usage: python compilation_manager.py --project MTN6_MCM01_UL1_UL3 --l5x-file "path/to/project.L5X" python compilation_manager.py --project MTN6_MCM04_CHUTE_LOAD --l5x-file "path/to/project.L5X" """ import os import sys import shutil import argparse from pathlib import Path from typing import Dict, List, Optional import glob class CompilationManager: """Manages dynamic compilation directory setup for different PLC projects.""" def __init__(self, compilation_dir: Path): """Initialize the compilation manager. Args: compilation_dir: Path to the L5X2ACD Compiler directory """ self.compilation_dir = Path(compilation_dir) self.l5x_to_acd_script = self.compilation_dir / "l5x_to_acd.py" # Ensure the compilation directory exists self.compilation_dir.mkdir(exist_ok=True) def wipe_compilation_files(self, preserve_core: bool = True) -> None: """Completely wipe compilation files, optionally preserving core scripts. Args: preserve_core: If True, preserve l5x_to_acd.py and __pycache__ """ print("๐Ÿงน Wiping existing compilation files...") # Files to always preserve core_files = { "l5x_to_acd.py", "compilation_manager.py", "setup_windows_sdk.bat", "logix_designer_sdk-2.0.1-py3-none-any.whl" } if preserve_core else set() # Directories to always preserve core_dirs = { "__pycache__" } if preserve_core else set() files_removed = 0 dirs_removed = 0 # Remove all files except core files for file_path in self.compilation_dir.glob("*"): if file_path.is_file(): if file_path.name not in core_files: try: file_path.unlink() files_removed += 1 print(f" โœ“ Removed file: {file_path.name}") except Exception as e: print(f" โš ๏ธ Could not remove {file_path.name}: {e}") elif file_path.is_dir(): if file_path.name not in core_dirs: try: shutil.rmtree(file_path) dirs_removed += 1 print(f" โœ“ Removed directory: {file_path.name}") except Exception as e: print(f" โš ๏ธ Could not remove {file_path.name}: {e}") print(f"๐Ÿงน Cleanup complete: {files_removed} files, {dirs_removed} directories removed") def copy_l5x_file(self, source_l5x: Path, project_name: str) -> Path: """Copy L5X file to compilation directory with project-specific naming. Args: source_l5x: Path to the source L5X file project_name: Name of the project for file naming Returns: Path to the copied L5X file in compilation directory """ if not source_l5x.exists(): raise FileNotFoundError(f"Source L5X file not found: {source_l5x}") # Create project-specific filename dest_filename = f"{project_name}.L5X" dest_l5x = self.compilation_dir / dest_filename print(f"๐Ÿ“ Copying L5X file: {source_l5x.name} โ†’ {dest_filename}") try: shutil.copy2(source_l5x, dest_l5x) file_size_mb = dest_l5x.stat().st_size / (1024 * 1024) print(f" โœ“ Copied successfully ({file_size_mb:.2f} MB)") return dest_l5x except Exception as e: raise RuntimeError(f"Failed to copy L5X file: {e}") def generate_batch_file(self, project_name: str, l5x_filename: str, compilation_options: Optional[Dict] = None) -> Path: """Generate project-specific batch file for compilation. Args: project_name: Name of the project l5x_filename: Name of the L5X file to compile compilation_options: Optional compilation settings Returns: Path to the generated batch file """ options = compilation_options or {} # Create project-specific batch filename batch_filename = f"compile_{project_name}.bat" batch_path = self.compilation_dir / batch_filename # Convert paths to Windows format for batch file compilation_dir_win = str(self.compilation_dir).replace('/mnt/c/', 'C:\\').replace('/', '\\') l5x_file_win = f"{compilation_dir_win}\\{l5x_filename}" # Create project-specific batch content batch_content = self._create_batch_content( project_name, l5x_file_win, compilation_dir_win, options ) print(f"๐Ÿ”ง Generating batch file: {batch_filename}") try: with open(batch_path, 'w', newline='\r\n') as f: # Windows line endings f.write(batch_content) print(f" โœ“ Generated successfully") return batch_path except Exception as e: raise RuntimeError(f"Failed to generate batch file: {e}") def _create_batch_content(self, project_name: str, l5x_file_win: str, compilation_dir_win: str, options: Dict) -> str: """Create the content for the batch file. Args: project_name: Name of the project l5x_file_win: Windows path to L5X file compilation_dir_win: Windows path to compilation directory options: Compilation options Returns: Batch file content as string """ # Determine project type for specialized handling project_type = self._detect_project_type(project_name) # Header content = f"@echo off\n" content += f"echo ====================================\n" content += f"echo PLC Compilation: {project_name}\n" content += f"echo Project Type: {project_type}\n" content += f"echo ====================================\n" content += f"echo.\n\n" # Set working directory content += f"cd /d \"{compilation_dir_win}\"\n" content += f"echo Working directory: %CD%\n" content += f"echo.\n\n" # Check if L5X file exists content += f"if not exist \"{l5x_file_win}\" (\n" content += f" echo ERROR: L5X file not found: {l5x_file_win}\n" content += f" pause\n" content += f" exit /b 1\n" content += f")\n\n" # Check for Python 3.12 content += f"echo Checking for Python 3.12...\n" content += f"py -3.12 --version >nul 2>&1\n" content += f"if errorlevel 1 (\n" content += f" echo.\n" content += f" echo ====================================\n" content += f" echo ERROR: Python 3.12 not found!\n" content += f" echo ====================================\n" content += f" echo.\n" content += f" echo This compilation requires Python 3.12 specifically.\n" content += f" echo.\n" content += f" echo INSTALLATION STEPS:\n" content += f" echo 1. Download Python 3.12 from: https://www.python.org/downloads/\n" content += f" echo 2. During installation, check 'Add Python to PATH'\n" content += f" echo 3. Verify installation: py -3.12 --version\n" content += f" echo.\n" content += f" echo ====================================\n" content += f" pause\n" content += f" exit /b 1\n" content += f")\n" content += f"echo โœ“ Python 3.12 found\n" # Check for Logix Designer SDK content += f"echo Checking for Logix Designer SDK...\n" content += f"py -3.12 -c \"import logix_designer_sdk\" 2>nul\n" content += f"if errorlevel 1 (\n" content += f" echo.\n" content += f" echo ====================================\n" content += f" echo ERROR: Logix Designer SDK not found!\n" content += f" echo ====================================\n" content += f" echo.\n" content += f" echo The Logix Designer SDK is required for L5X to ACD compilation.\n" content += f" echo.\n" content += f" echo INSTALLATION STEPS:\n" content += f" echo 1. Install the logix_designer_sdk package with Python 3.12:\n" content += f" echo py -3.12 -m pip install logix_designer_sdk-2.0.1-py3-none-any.whl\n" content += f" echo.\n" content += f" echo 2. Or run the setup script first:\n" content += f" echo setup_windows_sdk.bat\n" content += f" echo.\n" content += f" echo 3. Make sure you have Logix Designer installed on this Windows machine\n" content += f" echo.\n" content += f" echo ====================================\n" content += f" pause\n" content += f" exit /b 1\n" content += f")\n" content += f"echo โœ“ Logix Designer SDK found\n" content += f"echo.\n\n" # Show file info content += f"echo Input L5X file: {l5x_file_win}\n" content += f"for %%F in (\"{l5x_file_win}\") do echo File size: %%~zF bytes\n" content += f"echo.\n\n" # Compilation command with project-specific options compilation_cmd = f"py -3.12 l5x_to_acd.py \"{l5x_file_win}\"" # Add project-specific options if project_type == "MCM01" and options.get("enable_safety_validation", True): content += f"echo Enabling MCM01 safety validation...\n" elif project_type == "MCM04" and options.get("enable_feeder_optimization", True): content += f"echo Enabling MCM04 feeder optimization...\n" content += f"echo Starting compilation...\n" content += f"echo Command: {compilation_cmd}\n" content += f"echo.\n\n" # Execute compilation content += f"{compilation_cmd}\n\n" # Check compilation result expected_acd = l5x_file_win.replace('.L5X', '.ACD') content += f"if exist \"{expected_acd}\" (\n" content += f" echo.\n" content += f" echo ====================================\n" content += f" echo SUCCESS: Compilation completed!\n" content += f" echo Output: {expected_acd}\n" content += f" for %%F in (\"{expected_acd}\") do echo ACD size: %%~zF bytes\n" content += f" echo ====================================\n" content += f") else (\n" content += f" echo.\n" content += f" echo ====================================\n" content += f" echo ERROR: Compilation failed!\n" content += f" echo Expected output: {expected_acd}\n" content += f" echo ====================================\n" content += f")\n\n" # Footer content += f"echo.\n" content += f"echo Press any key to close...\n" content += f"pause\n" return content def _detect_project_type(self, project_name: str) -> str: """Detect the project type from the project name. Args: project_name: Name of the project Returns: Project type (MCM01, MCM04, MCM05, or UNKNOWN) """ project_upper = project_name.upper() if "MCM01" in project_upper: return "MCM01" elif "MCM04" in project_upper: return "MCM04" elif "MCM05" in project_upper: return "MCM05" else: return "UNKNOWN" def setup_compilation(self, source_l5x: Path, project_name: str, compilation_options: Optional[Dict] = None, wipe_existing: bool = True) -> Dict[str, Path]: """Complete compilation setup: wipe, copy, and generate batch file. Args: source_l5x: Path to the source L5X file project_name: Name of the project compilation_options: Optional compilation settings wipe_existing: Whether to wipe existing files first Returns: Dictionary with paths to generated files """ print(f"๐Ÿš€ Setting up compilation for project: {project_name}") print(f"๐Ÿ“‚ Compilation directory: {self.compilation_dir}") print(f"๐Ÿ“„ Source L5X: {source_l5x}") print() # Step 1: Wipe existing files if wipe_existing: self.wipe_compilation_files(preserve_core=True) print() # Step 2: Copy L5X file copied_l5x = self.copy_l5x_file(source_l5x, project_name) print() # Step 3: Generate batch file batch_file = self.generate_batch_file( project_name, copied_l5x.name, compilation_options ) print() result = { 'l5x_file': copied_l5x, 'batch_file': batch_file, 'compilation_dir': self.compilation_dir } print("โœ… Compilation setup complete!") print(f" L5X File: {copied_l5x}") print(f" Batch File: {batch_file}") print() print("๐ŸชŸ To compile on Windows:") print(f" 1. Run: {batch_file}") print(f" 2. Or double-click: {batch_file.name}") print() return result def main(): """Main entry point for the compilation manager.""" parser = argparse.ArgumentParser( description="Dynamic PLC Compilation Manager", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: # Setup compilation for MCM01 project python compilation_manager.py --project MTN6_MCM01_UL1_UL3 --l5x-file "../IO Tree Configuration Generator/generated_projects/MTN6_MCM01_UL1_UL3.L5X" # Setup compilation for MCM04 project python compilation_manager.py --project MTN6_MCM04_CHUTE_LOAD --l5x-file "../IO Tree Configuration Generator/generated_projects/MTN6_MCM04_CHUTE_LOAD.L5X" # Wipe only (no setup) python compilation_manager.py --wipe-only """ ) parser.add_argument('--project', '-p', help='Project name (e.g., MTN6_MCM01_UL1_UL3)') parser.add_argument('--l5x-file', '-l', type=Path, help='Path to the source L5X file') parser.add_argument('--compilation-dir', '-d', type=Path, default=Path(__file__).parent, help='Compilation directory (default: current directory)') parser.add_argument('--wipe-only', action='store_true', help='Only wipe existing files, do not setup compilation') parser.add_argument('--no-wipe', action='store_true', help='Do not wipe existing files before setup') parser.add_argument('--enable-safety-validation', action='store_true', help='Enable safety validation for MCM01 projects') parser.add_argument('--enable-feeder-optimization', action='store_true', help='Enable feeder optimization for MCM04 projects') args = parser.parse_args() # Create compilation manager manager = CompilationManager(args.compilation_dir) # Handle wipe-only mode if args.wipe_only: manager.wipe_compilation_files(preserve_core=True) print("โœ… Wipe completed.") return 0 # Validate arguments for full setup if not args.project: print("โŒ ERROR: --project is required for compilation setup") parser.print_help() return 1 if not args.l5x_file: print("โŒ ERROR: --l5x-file is required for compilation setup") parser.print_help() return 1 if not args.l5x_file.exists(): print(f"โŒ ERROR: L5X file not found: {args.l5x_file}") return 1 # Setup compilation options options = { 'enable_safety_validation': args.enable_safety_validation, 'enable_feeder_optimization': args.enable_feeder_optimization } try: # Setup compilation result = manager.setup_compilation( source_l5x=args.l5x_file, project_name=args.project, compilation_options=options, wipe_existing=not args.no_wipe ) print("๐ŸŽ‰ Ready for Windows compilation!") return 0 except Exception as e: print(f"โŒ ERROR: {e}") return 1 if __name__ == '__main__': sys.exit(main())