#!/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) # Create compiled_projects directory for organized output self.compiled_projects_dir = self.compilation_dir / "compiled_projects" self.compiled_projects_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" WARNING: 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" WARNING: Could not remove {file_path.name}: {e}") print(f"Cleanup complete: {files_removed} files, {dirs_removed} directories removed") def clean_old_project_files(self, current_project_name: str) -> None: """Clean old project files but keep current project files if they exist. Args: current_project_name: Name of the current project being compiled """ print("Cleaning old project 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" } # Directories to always preserve core_dirs = { "__pycache__" } # Current project files to potentially keep current_l5x = f"{current_project_name}.L5X" current_batch = f"compile_{current_project_name}.bat" current_acd = f"{current_project_name}.ACD" files_removed = 0 files_kept = 0 dirs_removed = 0 # Remove old project files, but keep current project files for file_path in self.compilation_dir.glob("*"): if file_path.is_file(): if file_path.name in core_files: # Always keep core files continue elif file_path.name in [current_l5x, current_batch, current_acd]: # Keep current project files files_kept += 1 print(f" ⚪ Keeping current project file: {file_path.name}") elif (file_path.name.endswith('.L5X') or file_path.name.startswith('compile_') or file_path.name.endswith('.ACD')): # Remove old project files try: file_path.unlink() files_removed += 1 print(f" Removed old project file: {file_path.name}") except Exception as e: print(f" WARNING: Could not remove {file_path.name}: {e}") else: # Remove other non-core files try: file_path.unlink() files_removed += 1 print(f" Removed file: {file_path.name}") except Exception as e: print(f" WARNING: 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" WARNING: Could not remove {file_path.name}: {e}") print(f"Cleanup complete: {files_removed} files removed, {files_kept} current files kept, {dirs_removed} directories removed") def copy_l5x_file(self, source_l5x: Path, project_name: str, force_overwrite: bool = False) -> Path: """Copy L5X file to project-specific compilation directory. Args: source_l5x: Path to the source L5X file project_name: Name of the project for file naming force_overwrite: Whether to overwrite existing file even if identical Returns: Path to the copied L5X file in project-specific compilation directory """ if not source_l5x.exists(): raise FileNotFoundError(f"Source L5X file not found: {source_l5x}") # Extract project prefix for folder organization import re project_match = re.match(r'^([A-Z0-9]+)_', project_name.upper()) project_prefix = project_match.group(1) if project_match else "UNKNOWN" # Ensure compiled projects directory exists first (in case it was removed during cleanup) self.compiled_projects_dir.mkdir(exist_ok=True) # Create project-specific directory project_compilation_dir = self.compiled_projects_dir / project_prefix project_compilation_dir.mkdir(exist_ok=True) # Create project-specific filename dest_filename = f"{project_name}.L5X" dest_l5x = project_compilation_dir / dest_filename # Check if we need to copy needs_copy = True if dest_l5x.exists() and not force_overwrite: # Compare file sizes and modification times source_stat = source_l5x.stat() dest_stat = dest_l5x.stat() if (source_stat.st_size == dest_stat.st_size and source_stat.st_mtime <= dest_stat.st_mtime): print(f"L5X file up to date: {dest_filename}") needs_copy = False if needs_copy: 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)") except Exception as e: raise RuntimeError(f"Failed to copy L5X file: {e}") return dest_l5x def generate_batch_file(self, project_name: str, l5x_file_path: Path, compilation_options: Optional[Dict] = None, force_regenerate: bool = False) -> Path: """Generate project-specific batch file for compilation. Args: project_name: Name of the project l5x_file_path: Path to the L5X file to compile compilation_options: Optional compilation settings force_regenerate: Whether to regenerate batch file even if it exists Returns: Path to the generated batch file """ options = compilation_options or {} # Extract project prefix for folder organization import re project_match = re.match(r'^([A-Z0-9]+)_', project_name.upper()) project_prefix = project_match.group(1) if project_match else "UNKNOWN" # Ensure compiled projects directory exists first (in case it was removed during cleanup) self.compiled_projects_dir.mkdir(exist_ok=True) # Create project-specific directory project_compilation_dir = self.compiled_projects_dir / project_prefix project_compilation_dir.mkdir(exist_ok=True) # Create project-specific batch filename batch_filename = f"compile_{project_name}.bat" batch_path = project_compilation_dir / batch_filename # Check if we need to regenerate needs_generation = True if batch_path.exists() and not force_regenerate: print(f"Batch file exists: {batch_filename}") needs_generation = False if needs_generation: # For ACD output, place it in the same project folder as the L5X file l5x_parent_dir = l5x_file_path.parent acd_output_file = l5x_parent_dir / f"{project_name}.ACD" # Convert paths to Windows format for batch file project_compilation_dir_win = str(project_compilation_dir).replace('/mnt/c/', 'C:\\').replace('/', '\\') l5x_file_win = str(l5x_file_path).replace('/mnt/c/', 'C:\\').replace('/', '\\') acd_output_win = str(acd_output_file).replace('/mnt/c/', 'C:\\').replace('/', '\\') # Create project-specific batch content with custom ACD output path batch_content = self._create_batch_content( project_name, l5x_file_win, project_compilation_dir_win, options, acd_output_win ) 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") except Exception as e: raise RuntimeError(f"Failed to generate batch file: {e}") return batch_path def _create_batch_content(self, project_name: str, l5x_file_win: str, compilation_dir_win: str, options: Dict, acd_output_win: str = None) -> 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 acd_output_win: Windows path for ACD output (optional) Returns: Batch file content as string """ # Use custom ACD output path if provided, otherwise default acd_output = acd_output_win or f"{project_name}.ACD" acd_display = Path(acd_output).name if acd_output_win else f"{project_name}.ACD" # Header content = f"@echo off\n" content += f"echo ====================================\n" content += f"echo PLC Compilation: {project_name}\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 (more robust check) content += f"echo Checking Logix Designer SDK...\n" content += f"py -3.12 -c \"import logix_designer_sdk; print('SDK import successful')\" >nul 2>&1\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 compilation_cmd = f"py -3.12 l5x_to_acd.py \"{l5x_file_win}\"" 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 setup_compilation(self, source_l5x: Path, project_name: str, compilation_options: Optional[Dict] = None, wipe_existing: bool = False, replace_mode: bool = True) -> Dict[str, Path]: """Complete compilation setup: clean old files, 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 ALL existing files first (old behavior) replace_mode: Whether to use smart replacement mode (new default behavior) 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(f"Mode: wipe_existing={wipe_existing}, replace_mode={replace_mode}") print() # Step 1: Clean files (either wipe all or smart clean) if wipe_existing: print("Using WIPE mode - removing all files") self.wipe_compilation_files(preserve_core=True) print() elif replace_mode: print("Using SMART REPLACE mode - keeping current project files") self.clean_old_project_files(project_name) print() else: print("Using NO-CLEAN mode - keeping all existing files") print() # Step 2: Copy L5X file (only if needed) copied_l5x = self.copy_l5x_file(source_l5x, project_name, force_overwrite=wipe_existing) print() # Step 3: Generate batch file (only if needed) batch_file = self.generate_batch_file( project_name, copied_l5x, compilation_options, force_regenerate=wipe_existing ) print() result = { 'l5x_file': copied_l5x, 'batch_file': batch_file, 'compilation_dir': self.compilation_dir } print("SUCCESS: 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 setup_project_folder_compilation(self, source_l5x: Path, project_name: str, compilation_options: Optional[Dict] = None) -> Path: """Create compilation batch file directly in the project folder. Args: source_l5x: Path to the source L5X file in the project folder project_name: Name of the project compilation_options: Optional compilation settings Returns: Path to the generated batch file """ print(f"Creating project folder compilation for: {project_name}") print(f"Source L5X: {source_l5x}") # Get the project directory (where the L5X file is located) project_dir = source_l5x.parent print(f"Project directory: {project_dir}") # Create batch file in the same directory as the L5X file batch_filename = f"compile_{project_name}.bat" batch_path = project_dir / batch_filename # Convert paths to Windows format for batch file l5x_file_win = str(source_l5x).replace('/mnt/c/', 'C:\\\\').replace('/', '\\\\') project_dir_win = str(project_dir).replace('/mnt/c/', 'C:\\\\').replace('/', '\\\\') compiler_dir_win = str(self.compilation_dir).replace('/mnt/c/', 'C:\\\\').replace('/', '\\\\') # Expected ACD output file in the same directory acd_output_win = f"{project_dir_win}\\\\{project_name}.ACD" # Create batch content batch_content = self._create_project_folder_batch_content( project_name, l5x_file_win, project_dir_win, compiler_dir_win, acd_output_win ) # Write batch file with open(batch_path, 'w', encoding='utf-8') as f: f.write(batch_content) print(f"Generated batch file: {batch_filename}") print(f"SUCCESS: Project compilation ready!") print(f"To compile on Windows:") print(f" cd \"{project_dir_win}\"") print(f" {batch_filename}") print() return batch_path def _create_project_folder_batch_content(self, project_name: str, l5x_file_win: str, project_dir_win: str, compiler_dir_win: str, acd_output_win: str) -> str: """Create batch file content for project folder compilation. Args: project_name: Name of the project l5x_file_win: Windows path to L5X file project_dir_win: Windows path to project directory compiler_dir_win: Windows path to compiler directory acd_output_win: Windows path for ACD output Returns: Batch file content as string """ content = "" # Header content += f"@echo off\n" content += f"echo ================================================================================\n" content += f"echo {project_name} Compilation\n" content += f"echo ================================================================================\n" content += f"echo.\n" content += f"echo Project: {project_name}\n" content += f"echo L5X File: {l5x_file_win}\n" content += f"echo Output ACD: {acd_output_win}\n" content += f"echo.\n\n" # Check Python 3.12 content += f"echo Checking Python 3.12...\n" content += f"py -3.12 --version >nul 2>&1\n" content += f"if errorlevel 1 (\n" content += f" echo ERROR: Python 3.12 not found\n" content += f" echo Please install Python 3.12 and ensure it's in PATH\n" content += f" pause\n" content += f" exit /b 1\n" content += f")\n" content += f"echo Python 3.12 found\n" content += f"echo.\n\n" # Check Logix Designer SDK content += f"echo Checking Logix Designer SDK...\n" content += f"py -3.12 -c \"import logix_designer_sdk; print('SDK import successful')\" >nul 2>&1\n" content += f"if errorlevel 1 (\n" content += f" echo ERROR: Logix Designer SDK not found\n" content += f" echo Please install Logix Designer SDK for Python\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" # Compilation command compilation_cmd = f"py -3.12 \"{compiler_dir_win}\\l5x_to_acd.py\" \"{l5x_file_win}\"" 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 results content += f"echo.\n" content += f"echo Checking compilation results...\n" content += f"if exist \"{acd_output_win}\" (\n" content += f" echo SUCCESS: ACD file created at {acd_output_win}\n" content += f" echo.\n" content += f" echo Ready for Studio 5000!\n" content += f") else (\n" content += f" echo ERROR: ACD file was not created\n" content += f" echo Check the output above for errors\n" content += f")\n" content += f"echo.\n" content += f"echo ================================================================================\n" content += f"pause\n" return content 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 (smart replace mode - default) python compilation_manager.py --project MTN6_MCM01_UL1_UL3 --l5x-file "../IO Tree Configuration Generator/generated_projects/MTN6_MCM01_UL1_UL3.L5X" # Setup compilation with old wipe behavior python compilation_manager.py --project MTN6_MCM04_CHUTE_LOAD --l5x-file "../IO Tree Configuration Generator/generated_projects/MTN6_MCM04_CHUTE_LOAD.L5X" --wipe # 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('--wipe', action='store_true', help='Use old wipe behavior (remove all files before setup)') parser.add_argument('--no-clean', action='store_true', help='Do not clean any existing files before setup') 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("SUCCESS: 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 = {} try: # Setup compilation with new smart replace mode result = manager.setup_compilation( source_l5x=args.l5x_file, project_name=args.project, compilation_options=options, wipe_existing=args.wipe, replace_mode=not args.no_clean and not args.wipe ) print("SUCCESS: Ready for Windows compilation!") return 0 except Exception as e: print(f"ERROR: {e}") return 1 if __name__ == '__main__': sys.exit(main())