504 lines
20 KiB
Python
504 lines
20 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 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" ⚠️ 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" ⚠️ 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 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 compilation directory with project-specific naming.
|
|
|
|
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 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
|
|
|
|
# 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_filename: str,
|
|
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_filename: Name of 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 {}
|
|
|
|
# Create project-specific batch filename
|
|
batch_filename = f"compile_{project_name}.bat"
|
|
batch_path = self.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:
|
|
# 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")
|
|
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) -> 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
|
|
"""
|
|
# 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
|
|
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
|
|
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.name, compilation_options, force_regenerate=wipe_existing
|
|
)
|
|
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 (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("✅ 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("🎉 Ready for Windows compilation!")
|
|
return 0
|
|
|
|
except Exception as e:
|
|
print(f"❌ ERROR: {e}")
|
|
return 1
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main()) |