PLC_Generation/L5X2ACD Compiler/compilation_manager.py
2025-09-02 11:13:29 +04:00

670 lines
28 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)
# 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())