PLC_Generation/L5X2ACD Compiler/compilation_manager.py
2025-08-05 14:38:54 +04:00

433 lines
17 KiB
Python

#!/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())