diff --git a/.gitignore b/.gitignore index f321b0e7a4..670e419b08 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ nghdl* tags build/ dist/ +logs/*.log diff --git a/README.md b/README.md index fb42bffe36..e934a49af1 100644 --- a/README.md +++ b/README.md @@ -598,6 +598,35 @@ A huge thank you to all **149+ amazing people** who have contributed to eSim!
149+ contributors and counting! View all contributors →
--- +## 🛠️ Automated Tool Manager + +The eSim repository includes an Automated Tool Manager for managing external EDA tools required by eSim on Ubuntu Linux systems. + +The Tool Manager supports: + +- **Ngspice** — Circuit Simulation +- **Verilator** — Digital Verification +- **GHDL** — VHDL Simulation +- **KiCad** — PCB Design + +### Features + +- Detect installed tools +- Install missing tools using APT +- Check dependencies +- Detect installed and available package versions +- Check for updates +- Upgrade installed tools +- Manage Tool Manager configuration +- Record tool-management operations +- Run automated unit tests + +### Running the Tool Manager + +From the eSim repository root: + +```bash +python3 -m tools.esim_tool_manager ## 📞 Contact & Support diff --git a/config/esim_manager.ini b/config/esim_manager.ini new file mode 100644 index 0000000000..e3bc773e7e --- /dev/null +++ b/config/esim_manager.ini @@ -0,0 +1,15 @@ +[system] +package_manager = apt + +[esim] +installation_path = /opt/esim + +[updates] +auto_check = true + +[tools] +ngspice = ngspice +verilator = verilator +ghdl = ghdl +kicad = kicad + diff --git a/docs/Task_5_Tool_Manager_Design.md b/docs/Task_5_Tool_Manager_Design.md new file mode 100644 index 0000000000..89c3834b78 --- /dev/null +++ b/docs/Task_5_Tool_Manager_Design.md @@ -0,0 +1,232 @@ +# Automated Tool Manager for eSim + +## 1. Overview + +The Automated Tool Manager is a Python-based command-line utility developed as part of the eSim Semester Long Internship Task 5. + +The purpose of the Tool Manager is to simplify the management of external tools required by eSim. + +The manager provides a centralized interface for detecting, installing, checking, updating, upgrading, and monitoring the required EDA tools. + +Supported tools: + +- Ngspice +- Verilator +- GHDL +- KiCad + +The implementation is designed for Ubuntu Linux systems using APT. + +## 2. Objectives + +The main objectives are: + +1. Detect installed eSim dependencies. +2. Identify missing dependencies. +3. Install supported tools using APT. +4. Detect installed package versions. +5. Check whether newer package versions are available. +6. Upgrade supported packages. +7. Maintain configuration settings. +8. Record tool-management activities. +9. Provide a simple command-line interface. +10. Provide automated tests for core functionality. + +## 3. Architecture + +The Tool Manager consists of independent Python modules: + + cli.py + | + +-- detector.py + +-- installer.py + +-- dependency_checker.py + +-- version_checker.py + +-- update_checker.py + +-- upgrade_manager.py + +-- config.py + +-- logger.py + | + +-- APT / dpkg + | + +-- Ngspice + +-- Verilator + +-- GHDL + +-- KiCad + +## 4. Modules + +### detector.py + +Detects whether supported tools are installed and retrieves their versions. + +Supported tools: + +- Ngspice +- Verilator +- GHDL +- KiCad + +KiCad uses dpkg-query for version detection because it does not support the standard --version option. + +### installer.py + +Installs supported tools using APT. + +Supported packages: + +- ngspice +- verilator +- ghdl +- kicad + +The installer verifies the installation after completion. + +### dependency_checker.py + +Checks whether all required eSim tools are installed. + +The checker reports: + +- Installed tools +- Missing tools +- Overall system status + +### version_checker.py + +Retrieves installed package versions and APT candidate versions. + +### update_checker.py + +Compares installed Debian package versions with the APT candidate versions using: + + dpkg --compare-versions + +Possible states include: + +- Up to date +- Update available +- Not installed +- Unable to check + +### upgrade_manager.py + +Upgrades individual packages using APT and verifies the installed package version afterward. + +### config.py + +Manages configuration using Python's configparser. + +Configuration file: + + config/esim_manager.ini + +### logger.py + +Records Tool Manager operations in: + + logs/tool_manager.log + +Generated logs are excluded from Git. + +### cli.py + +Provides the interactive menu: + + 1. Scan Tools + 2. Install Tool + 3. Dependency Check + 4. System Information + 5. Check Updates + 6. Upgrade Tool + 7. Configuration + 8. View Logs + 9. Exit + +### __main__.py + +Provides the package entry point: + + python3 -m tools.esim_tool_manager + +## 5. Configuration + +The default configuration is: + + [system] + package_manager = apt + + [esim] + installation_path = /opt/esim + + [updates] + auto_check = true + + [tools] + ngspice = ngspice + verilator = verilator + ghdl = ghdl + kicad = kicad + +## 6. Testing + +Tests are located in: + + tests/esim_tool_manager/ + +The project uses Python's built-in unittest framework. + +Run: + + python3 -m unittest discover -s tests -v + +Current tests cover: + +- Installed tool detection +- Missing tool detection +- Supported tool validation +- Update comparison +- Up-to-date comparison +- Missing version handling + +Current validation result: + + Ran 6 tests + OK + +## 7. System Requirements + +The current implementation targets: + +- Ubuntu Linux +- Python 3.x +- APT package manager +- dpkg + +Supported tools: + +- Ngspice +- Verilator +- GHDL +- KiCad + +## 8. Security Considerations + +Installation and upgrade operations use sudo because system package management requires administrative privileges. + +The Tool Manager does not store passwords or authentication credentials. + +Generated runtime logs are excluded from version control. + +## 9. Future Improvements + +Possible future improvements include: + +- Graphical user interface +- Windows and macOS support +- Automatic rollback +- Remote package repository support +- Scheduled updates +- More comprehensive test coverage +- Additional eSim dependency support +- Integration with the existing eSim installation workflow diff --git a/tests/esim_tool_manager/__init__.py b/tests/esim_tool_manager/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/esim_tool_manager/test_detector.py b/tests/esim_tool_manager/test_detector.py new file mode 100644 index 0000000000..53fabdb522 --- /dev/null +++ b/tests/esim_tool_manager/test_detector.py @@ -0,0 +1,58 @@ +import unittest +from unittest.mock import patch + +from tools.esim_tool_manager.detector import ToolDetector + + +class TestToolDetector(unittest.TestCase): + + def test_detect_installed_tool(self): + detector = ToolDetector() + + with patch( + "shutil.which", + return_value="/usr/bin/ngspice", + ): + with patch.object( + detector, + "_get_version", + return_value="ngspice version 45.2", + ): + result = detector.detect_tool( + "ngspice", + "ngspice", + ) + + self.assertTrue(result.installed) + self.assertEqual(result.name, "ngspice") + self.assertEqual( + result.version, + "ngspice version 45.2", + ) + + def test_detect_missing_tool(self): + detector = ToolDetector() + + with patch( + "shutil.which", + return_value=None, + ): + result = detector.detect_tool( + "ngspice", + "ngspice", + ) + + self.assertFalse(result.installed) + self.assertIsNone(result.version) + + def test_supported_tools(self): + detector = ToolDetector() + + self.assertIn("ngspice", detector.TOOLS) + self.assertIn("verilator", detector.TOOLS) + self.assertIn("ghdl", detector.TOOLS) + self.assertIn("kicad", detector.TOOLS) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/esim_tool_manager/test_update_checker.py b/tests/esim_tool_manager/test_update_checker.py new file mode 100644 index 0000000000..0b6747d2e1 --- /dev/null +++ b/tests/esim_tool_manager/test_update_checker.py @@ -0,0 +1,62 @@ +import unittest +from unittest.mock import patch + +from tools.esim_tool_manager.update_checker import ( + UpdateChecker, +) + + +class TestUpdateChecker(unittest.TestCase): + + def test_compare_versions_update_available(self): + checker = UpdateChecker() + + with patch( + "subprocess.run" + ) as mock_run: + + mock_run.return_value.returncode = 0 + + result = checker.compare_versions( + "1.0-1", + "2.0-1", + ) + + self.assertTrue(result) + + def test_compare_versions_up_to_date(self): + checker = UpdateChecker() + + with patch( + "subprocess.run" + ) as mock_run: + + mock_run.return_value.returncode = 1 + + result = checker.compare_versions( + "2.0-1", + "2.0-1", + ) + + self.assertFalse(result) + + def test_compare_versions_missing_version(self): + checker = UpdateChecker() + + self.assertFalse( + checker.compare_versions( + None, + "2.0-1", + ) + ) + + self.assertFalse( + checker.compare_versions( + "1.0-1", + None, + ) + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/esim_tool_manager/__init__.py b/tools/esim_tool_manager/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/esim_tool_manager/__main__.py b/tools/esim_tool_manager/__main__.py new file mode 100644 index 0000000000..1a4645a8ae --- /dev/null +++ b/tools/esim_tool_manager/__main__.py @@ -0,0 +1,10 @@ +from .cli import ToolManagerCLI + + +def main() -> None: + """Start the eSim Tool Manager.""" + ToolManagerCLI().run() + + +if __name__ == "__main__": + main() diff --git a/tools/esim_tool_manager/cli.py b/tools/esim_tool_manager/cli.py new file mode 100644 index 0000000000..2e5d659f22 --- /dev/null +++ b/tools/esim_tool_manager/cli.py @@ -0,0 +1,290 @@ +from .config import ConfigManager +from .dependency_checker import DependencyChecker +from .detector import ToolDetector +from .installer import ToolInstaller +from .logger import ToolManagerLogger +from .update_checker import UpdateChecker +from .upgrade_manager import UpgradeManager + + +class ToolManagerCLI: + """Interactive command-line interface for eSim Tool Manager.""" + + TOOLS = [ + "ngspice", + "verilator", + "ghdl", + "kicad", + ] + + def __init__(self) -> None: + self.detector = ToolDetector() + self.installer = ToolInstaller() + self.dependency_checker = DependencyChecker() + self.update_checker = UpdateChecker() + self.upgrade_manager = UpgradeManager() + self.config = ConfigManager() + self.logger = ToolManagerLogger() + + def display_menu(self) -> None: + """Display the main menu.""" + + print("\n") + print("=" * 60) + print(" eSim Automated Tool Manager") + print("=" * 60) + print("1. Scan Tools") + print("2. Install Tool") + print("3. Dependency Check") + print("4. System Information") + print("5. Check Updates") + print("6. Upgrade Tool") + print("7. Configuration") + print("8. View Logs") + print("9. Exit") + print("=" * 60) + + def scan_tools(self) -> None: + """Display detected tools.""" + + print("\neSim Tool Detection") + print("=" * 60) + + for tool in self.detector.scan(): + if tool.installed: + version = tool.version or "Version unknown" + print( + f"{tool.name:<12}: " + f"Installed ({version})" + ) + else: + print( + f"{tool.name:<12}: " + "Not installed" + ) + + def select_tool(self) -> str | None: + """Ask the user to select a supported tool.""" + + print("\nSelect Tool") + print("-" * 40) + + for index, tool in enumerate(self.TOOLS, start=1): + print(f"{index}. {tool}") + + choice = input("\nEnter choice: ").strip() + + try: + number = int(choice) + + if 1 <= number <= len(self.TOOLS): + return self.TOOLS[number - 1] + + except ValueError: + pass + + print("Invalid tool selection.") + return None + + def install_tool(self) -> None: + """Install a selected tool.""" + + tool = self.select_tool() + + if tool is None: + return + + self.logger.install_start(tool) + + success = self.installer.install(tool) + + if success: + status = self.detector.detect_tool( + tool, + self.installer.PACKAGES[tool], + ) + + version = status.version or "unknown" + + self.logger.install_success( + tool, + version, + ) + else: + self.logger.install_failed(tool) + + def dependency_check(self) -> None: + """Run the dependency checker.""" + + self.dependency_checker.check() + + def system_information(self) -> None: + """Display basic system information.""" + + import platform + import sys + + print("\neSim System Information") + print("=" * 60) + print(f"Operating System : {platform.system()}") + print(f"OS Version : {platform.release()}") + print(f"Architecture : {platform.machine()}") + print(f"Python Version : {sys.version.split()[0]}") + print( + f"Package Manager : " + f"{self.config.get('system', 'package_manager')}" + ) + print( + f"eSim Path : " + f"{self.config.get('esim', 'installation_path')}" + ) + + def check_updates(self) -> None: + """Check updates for all supported tools.""" + + self.logger.update_check() + + print("\neSim Update Check") + print("=" * 60) + + for status in self.update_checker.check_all(): + + print(f"\n{status.tool}") + + print( + f" Installed : " + f"{status.installed_version or 'Not installed'}" + ) + + print( + f" Available : " + f"{status.available_version or 'Not available'}" + ) + + if status.installed_version is None: + print(" Status : Not installed") + + elif status.available_version is None: + print(" Status : Unable to check") + + elif status.update_available: + print(" Status : Update available") + + else: + print(" Status : Up to date") + + def upgrade_tool(self) -> None: + """Upgrade a selected tool.""" + + tool = self.select_tool() + + if tool is None: + return + + self.logger.upgrade_start(tool) + + success = self.upgrade_manager.upgrade(tool) + + if success: + version = ( + self.upgrade_manager + ._get_package_version( + self.upgrade_manager.PACKAGES[tool] + ) + or "unknown" + ) + + self.logger.upgrade_success( + tool, + version, + ) + else: + self.logger.upgrade_failed(tool) + + def configuration(self) -> None: + """Display configuration.""" + + self.config.display() + + def view_logs(self) -> None: + """Display the tool manager log.""" + + log_path = self.logger.log_path + + print("\neSim Tool Manager Logs") + print("=" * 60) + + if not log_path.exists(): + print("No log file found.") + return + + try: + content = log_path.read_text( + encoding="utf-8" + ) + + if content.strip(): + print(content) + else: + print("Log file is empty.") + + except OSError as error: + print(f"Unable to read log file: {error}") + + def run(self) -> None: + """Run the interactive CLI.""" + + self.logger.info( + "SYSTEM", + "Tool Manager started", + ) + + while True: + self.display_menu() + + choice = input( + "Enter your choice: " + ).strip() + + if choice == "1": + self.scan_tools() + + elif choice == "2": + self.install_tool() + + elif choice == "3": + self.dependency_check() + + elif choice == "4": + self.system_information() + + elif choice == "5": + self.check_updates() + + elif choice == "6": + self.upgrade_tool() + + elif choice == "7": + self.configuration() + + elif choice == "8": + self.view_logs() + + elif choice == "9": + self.logger.info( + "SYSTEM", + "Tool Manager stopped", + ) + + print("\nExiting eSim Tool Manager...") + break + + else: + print( + "\nInvalid choice. " + "Please select 1-9." + ) + + +if __name__ == "__main__": + ToolManagerCLI().run() diff --git a/tools/esim_tool_manager/config.py b/tools/esim_tool_manager/config.py new file mode 100644 index 0000000000..5872047c48 --- /dev/null +++ b/tools/esim_tool_manager/config.py @@ -0,0 +1,118 @@ +from configparser import ConfigParser +from pathlib import Path + + +class ConfigManager: + """Manage eSim Tool Manager configuration.""" + + DEFAULT_CONFIG = { + "system": { + "package_manager": "apt", + }, + "esim": { + "installation_path": "/opt/esim", + }, + "updates": { + "auto_check": "true", + }, + "tools": { + "ngspice": "ngspice", + "verilator": "verilator", + "ghdl": "ghdl", + "kicad": "kicad", + }, + } + + def __init__(self, config_path: str = "config/esim_manager.ini"): + self.config_path = Path(config_path) + self.config = ConfigParser() + + self._ensure_config_directory() + self.load() + + def _ensure_config_directory(self) -> None: + """Create the configuration directory if required.""" + + self.config_path.parent.mkdir( + parents=True, + exist_ok=True, + ) + + def create_default_config(self) -> None: + """Create a default configuration file.""" + + self.config.clear() + + for section, values in self.DEFAULT_CONFIG.items(): + self.config[section] = values + + self.save() + + def load(self) -> None: + """Load configuration or create defaults.""" + + if not self.config_path.exists(): + self.create_default_config() + return + + self.config.read(self.config_path) + + def save(self) -> None: + """Save configuration to disk.""" + + with self.config_path.open( + "w", + encoding="utf-8", + ) as config_file: + self.config.write(config_file) + + def get( + self, + section: str, + option: str, + fallback=None, + ): + """Get a configuration value.""" + + return self.config.get( + section, + option, + fallback=fallback, + ) + + def set( + self, + section: str, + option: str, + value: str, + ) -> None: + """Set and save a configuration value.""" + + if not self.config.has_section(section): + self.config.add_section(section) + + self.config.set( + section, + option, + value, + ) + + self.save() + + def display(self) -> None: + """Display all configuration settings.""" + + print("eSim Tool Manager Configuration") + print("=" * 60) + + for section in self.config.sections(): + print(f"\n[{section}]") + + for option, value in self.config.items(section): + print(f"{option} = {value}") + + +if __name__ == "__main__": + manager = ConfigManager() + + manager.display() diff --git a/tools/esim_tool_manager/dependency_checker.py b/tools/esim_tool_manager/dependency_checker.py new file mode 100644 index 0000000000..bb743b4f82 --- /dev/null +++ b/tools/esim_tool_manager/dependency_checker.py @@ -0,0 +1,55 @@ +from .detector import ToolDetector + + +class DependencyChecker: + """Check whether required eSim tools are available.""" + + REQUIRED_TOOLS = [ + "ngspice", + "verilator", + "ghdl", + "kicad", + ] + + def __init__(self) -> None: + self.detector = ToolDetector() + + def check(self) -> bool: + """Check all required tools and return overall status.""" + + results = self.detector.scan() + all_available = True + + print("eSim Dependency Check") + print("=" * 60) + + for tool in results: + if tool.name not in self.REQUIRED_TOOLS: + continue + + if tool.installed: + version = tool.version or "Version unknown" + print( + f"{tool.name:<12}: " + f"Installed ({version})" + ) + else: + print( + f"{tool.name:<12}: " + "Missing" + ) + all_available = False + + print("=" * 60) + + if all_available: + print("System Status: READY") + else: + print("System Status: MISSING DEPENDENCIES") + + return all_available + + +if __name__ == "__main__": + checker = DependencyChecker() + checker.check() diff --git a/tools/esim_tool_manager/detector.py b/tools/esim_tool_manager/detector.py new file mode 100644 index 0000000000..80ed97661c --- /dev/null +++ b/tools/esim_tool_manager/detector.py @@ -0,0 +1,130 @@ +import shutil +import subprocess +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class ToolStatus: + name: str + command: str + installed: bool + version: Optional[str] = None + + +class ToolDetector: + """Detect installed eSim external tools.""" + + TOOLS = { + "ngspice": "ngspice", + "verilator": "verilator", + "ghdl": "ghdl", + "kicad": "kicad", + } + + def detect_tool(self, name: str, command: str) -> ToolStatus: + """Detect whether a tool is installed.""" + + executable = shutil.which(command) + + if executable is None: + return ToolStatus( + name=name, + command=command, + installed=False, + ) + + if command == "kicad": + version = self._get_kicad_version() + else: + version = self._get_version(command) + + return ToolStatus( + name=name, + command=command, + installed=True, + version=version, + ) + + def _get_version(self, command: str) -> Optional[str]: + """Get the version of a standard command-line tool.""" + + try: + result = subprocess.run( + [command, "--version"], + capture_output=True, + text=True, + timeout=10, + check=False, + ) + + output = result.stdout.strip() or result.stderr.strip() + + if not output: + return None + + return output.splitlines()[0] + + except (subprocess.SubprocessError, OSError): + return None + + def _get_kicad_version(self) -> Optional[str]: + """ + Get KiCad version from the installed APT package. + + KiCad does not support the standard --version argument, + so dpkg-query is used instead. + """ + + try: + result = subprocess.run( + [ + "dpkg-query", + "-W", + "-f=${Version}", + "kicad", + ], + capture_output=True, + text=True, + timeout=10, + check=False, + ) + + version = result.stdout.strip() + + if version: + return version + + except (subprocess.SubprocessError, OSError): + pass + + return None + + def scan(self) -> list[ToolStatus]: + """Scan all supported eSim tools.""" + + return [ + self.detect_tool(name, command) + for name, command in self.TOOLS.items() + ] + + +if __name__ == "__main__": + detector = ToolDetector() + + print("eSim Tool Detection") + print("=" * 60) + + for tool in detector.scan(): + if tool.installed: + version = tool.version or "Version unknown" + + print( + f"{tool.name:<12}: " + f"Installed ({version})" + ) + else: + print( + f"{tool.name:<12}: " + "Not installed" + ) diff --git a/tools/esim_tool_manager/installer.py b/tools/esim_tool_manager/installer.py new file mode 100644 index 0000000000..eef44e6a91 --- /dev/null +++ b/tools/esim_tool_manager/installer.py @@ -0,0 +1,125 @@ +import shutil +import subprocess +from typing import Optional + +from .detector import ToolDetector + + +class ToolInstaller: + """Install eSim external tools using APT.""" + + PACKAGES = { + "ngspice": "ngspice", + "verilator": "verilator", + "ghdl": "ghdl", + "kicad": "kicad", + } + + def __init__(self) -> None: + self.detector = ToolDetector() + + def is_apt_available(self) -> bool: + """Check whether APT is available on the system.""" + return shutil.which("apt") is not None + + def install(self, tool_name: str) -> bool: + """Install a supported tool using APT.""" + + tool_name = tool_name.lower() + + if tool_name not in self.PACKAGES: + print(f"Unsupported tool: {tool_name}") + return False + + if not self.is_apt_available(): + print("APT package manager is not available.") + return False + + package = self.PACKAGES[tool_name] + + print(f"Installing {tool_name}...") + print(f"APT package: {package}") + + try: + result = subprocess.run( + ["sudo", "apt", "update"], + check=False, + ) + + if result.returncode != 0: + print("Failed to update APT package information.") + return False + + result = subprocess.run( + ["sudo", "apt", "install", "-y", package], + check=False, + ) + + if result.returncode != 0: + print(f"Failed to install {tool_name}.") + return False + + print(f"{tool_name} installation completed.") + + return self.verify_installation(tool_name) + + except (OSError, subprocess.SubprocessError) as error: + print(f"Installation error: {error}") + return False + + def verify_installation(self, tool_name: str) -> bool: + """Verify that a tool is installed after installation.""" + + tool_name = tool_name.lower() + + if tool_name not in self.PACKAGES: + return False + + command = self.PACKAGES[tool_name] + + if shutil.which(command) is None: + print(f"Verification failed: {tool_name} not found.") + return False + + status = self.detector.detect_tool( + tool_name, + command, + ) + + if status.installed: + print( + f"Verification successful: " + f"{tool_name} " + f"({status.version or 'version unknown'})" + ) + return True + + print(f"Verification failed: {tool_name}") + return False + + def install_missing_tools(self) -> None: + """Install all supported tools that are currently missing.""" + + for tool_name in self.PACKAGES: + command = self.PACKAGES[tool_name] + + if shutil.which(command): + print(f"{tool_name}: already installed") + continue + + print(f"{tool_name}: missing") + self.install(tool_name) + + +if __name__ == "__main__": + installer = ToolInstaller() + + print("eSim Tool Installer") + print("=" * 60) + + print("\nSupported tools:") + for tool in installer.PACKAGES: + print(f"- {tool}") + + print("\nExample:") + print("python3 -m tools.esim_tool_manager.installer") diff --git a/tools/esim_tool_manager/logger.py b/tools/esim_tool_manager/logger.py new file mode 100644 index 0000000000..6bf7660601 --- /dev/null +++ b/tools/esim_tool_manager/logger.py @@ -0,0 +1,130 @@ +import logging +from pathlib import Path + + +class ToolManagerLogger: + """Logging system for the eSim Tool Manager.""" + + def __init__( + self, + log_path: str = "logs/tool_manager.log", + ) -> None: + self.log_path = Path(log_path) + + self.log_path.parent.mkdir( + parents=True, + exist_ok=True, + ) + + self.logger = logging.getLogger( + "esim_tool_manager" + ) + + self.logger.setLevel(logging.INFO) + + if not self.logger.handlers: + handler = logging.FileHandler( + self.log_path, + encoding="utf-8", + ) + + formatter = logging.Formatter( + "%(asctime)s | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + + handler.setFormatter(formatter) + self.logger.addHandler(handler) + + def info(self, event: str, message: str) -> None: + """Record an informational event.""" + + self.logger.info( + "%s | %s", + event, + message, + ) + + def install_start(self, tool: str) -> None: + self.info( + "INSTALL_START", + f"Installing {tool}", + ) + + def install_success( + self, + tool: str, + version: str, + ) -> None: + self.info( + "INSTALL_SUCCESS", + f"{tool} installed version={version}", + ) + + def install_failed( + self, + tool: str, + ) -> None: + self.info( + "INSTALL_FAILED", + f"Failed to install {tool}", + ) + + def update_check(self) -> None: + self.info( + "UPDATE_CHECK", + "Checking package updates", + ) + + def upgrade_start(self, tool: str) -> None: + self.info( + "UPGRADE_START", + f"Upgrading {tool}", + ) + + def upgrade_success( + self, + tool: str, + version: str, + ) -> None: + self.info( + "UPGRADE_SUCCESS", + f"{tool} upgraded version={version}", + ) + + def upgrade_failed( + self, + tool: str, + ) -> None: + self.info( + "UPGRADE_FAILED", + f"Failed to upgrade {tool}", + ) + + +if __name__ == "__main__": + logger = ToolManagerLogger() + + logger.info( + "SYSTEM", + "Tool Manager logging initialized", + ) + + logger.install_start("Verilator") + + logger.install_success( + "Verilator", + "5.032", + ) + + logger.update_check() + + logger.upgrade_start("Verilator") + + logger.upgrade_success( + "Verilator", + "5.032", + ) + + print("Logging test completed.") + print("Log file: logs/tool_manager.log") diff --git a/tools/esim_tool_manager/update_checker.py b/tools/esim_tool_manager/update_checker.py new file mode 100644 index 0000000000..e0998689c6 --- /dev/null +++ b/tools/esim_tool_manager/update_checker.py @@ -0,0 +1,162 @@ +import subprocess +from dataclasses import dataclass +from typing import Optional + +from .version_checker import VersionChecker + + +@dataclass +class UpdateStatus: + tool: str + installed_version: Optional[str] + available_version: Optional[str] + update_available: bool + + +class UpdateChecker: + """Check whether eSim tools have available APT updates.""" + + PACKAGES = { + "ngspice": "ngspice", + "verilator": "verilator", + "ghdl": "ghdl", + "kicad": "kicad", + } + + def __init__(self) -> None: + self.version_checker = VersionChecker() + + def get_dpkg_version( + self, + package: str, + ) -> Optional[str]: + """Get the installed Debian package version.""" + + try: + result = subprocess.run( + [ + "dpkg-query", + "-W", + "-f=${Version}", + package, + ], + capture_output=True, + text=True, + timeout=10, + check=False, + ) + + version = result.stdout.strip() + + if not version: + return None + + return version + + except (subprocess.SubprocessError, OSError): + return None + + def compare_versions( + self, + installed: Optional[str], + available: Optional[str], + ) -> bool: + """Return True if the available version is newer.""" + + if not installed or not available: + return False + + try: + result = subprocess.run( + [ + "dpkg", + "--compare-versions", + installed, + "lt", + available, + ], + check=False, + ) + + return result.returncode == 0 + + except (subprocess.SubprocessError, OSError): + return False + + def check_tool( + self, + tool_name: str, + ) -> UpdateStatus: + """Check update status for one tool.""" + + tool_name = tool_name.lower() + + if tool_name not in self.PACKAGES: + raise ValueError( + f"Unsupported tool: {tool_name}" + ) + + package = self.PACKAGES[tool_name] + + installed_version = self.get_dpkg_version( + package + ) + + available_version = ( + self.version_checker.get_available_version( + package + ) + ) + + update_available = self.compare_versions( + installed_version, + available_version, + ) + + return UpdateStatus( + tool=tool_name, + installed_version=installed_version, + available_version=available_version, + update_available=update_available, + ) + + def check_all(self) -> list[UpdateStatus]: + """Check update status for all supported tools.""" + + return [ + self.check_tool(tool) + for tool in self.PACKAGES + ] + + +if __name__ == "__main__": + checker = UpdateChecker() + + print("eSim Update Checker") + print("=" * 70) + + for status in checker.check_all(): + + print(f"\n{status.tool}") + + print( + f" Installed : " + f"{status.installed_version or 'Not installed'}" + ) + + print( + f" Available : " + f"{status.available_version or 'Not available'}" + ) + + if status.installed_version is None: + print(" Status : Not installed") + + elif status.available_version is None: + print(" Status : Unable to check") + + elif status.update_available: + print(" Status : Update available") + + else: + print(" Status : Up to date") diff --git a/tools/esim_tool_manager/upgrade_manager.py b/tools/esim_tool_manager/upgrade_manager.py new file mode 100644 index 0000000000..fcbd9360fc --- /dev/null +++ b/tools/esim_tool_manager/upgrade_manager.py @@ -0,0 +1,169 @@ +import shutil +import subprocess + +from .detector import ToolDetector +from .update_checker import UpdateChecker + + +class UpgradeManager: + """Upgrade eSim tools using APT.""" + + PACKAGES = { + "ngspice": "ngspice", + "verilator": "verilator", + "ghdl": "ghdl", + "kicad": "kicad", + } + + def __init__(self) -> None: + self.detector = ToolDetector() + self.update_checker = UpdateChecker() + + def upgrade(self, tool_name: str) -> bool: + """Upgrade a supported tool.""" + + tool_name = tool_name.lower() + + if tool_name not in self.PACKAGES: + print(f"Unsupported tool: {tool_name}") + return False + + if shutil.which("apt") is None: + print("APT package manager is not available.") + return False + + package = self.PACKAGES[tool_name] + + print(f"Checking updates for {tool_name}...") + + status = self.update_checker.check_tool(tool_name) + + if status.installed_version is None: + print( + f"{tool_name} is not installed. " + "Use the installer first." + ) + return False + + if status.available_version is None: + print( + f"Unable to determine the available " + f"version for {tool_name}." + ) + return False + + if not status.update_available: + print( + f"{tool_name} is already up to date " + f"({status.installed_version})." + ) + return True + + print( + f"Updating {tool_name}: " + f"{status.installed_version} -> " + f"{status.available_version}" + ) + + try: + result = subprocess.run( + [ + "sudo", + "apt", + "install", + "--only-upgrade", + "-y", + package, + ], + check=False, + ) + + if result.returncode != 0: + print(f"Upgrade failed for {tool_name}.") + return False + + print(f"{tool_name} upgrade completed.") + + return self.verify_upgrade(tool_name) + + except (OSError, subprocess.SubprocessError) as error: + print(f"Upgrade error: {error}") + return False + + def verify_upgrade(self, tool_name: str) -> bool: + """Verify that the tool remains installed after upgrade.""" + + tool_name = tool_name.lower() + + if tool_name not in self.PACKAGES: + return False + + package = self.PACKAGES[tool_name] + + if shutil.which(package) is None: + print( + f"Verification failed: " + f"{tool_name} is not available." + ) + return False + + version = self._get_package_version(package) + + if version: + print( + f"Verification successful: " + f"{tool_name} version {version}" + ) + return True + + print( + f"{tool_name} is installed, " + "but its package version could not be determined." + ) + + return True + + @staticmethod + def _get_package_version(package: str) -> str | None: + """Get installed package version using dpkg-query.""" + + try: + result = subprocess.run( + [ + "dpkg-query", + "-W", + "-f=${Version}", + package, + ], + capture_output=True, + text=True, + timeout=10, + check=False, + ) + + version = result.stdout.strip() + + return version or None + + except (OSError, subprocess.SubprocessError): + return None + + +if __name__ == "__main__": + manager = UpgradeManager() + + print("eSim Upgrade Manager") + print("=" * 60) + + print("\nSupported tools:") + + for tool in manager.PACKAGES: + print(f"- {tool}") + + print("\nExample:") + print( + "manager = UpgradeManager()" + ) + print( + "manager.upgrade('verilator')" + ) diff --git a/tools/esim_tool_manager/version_checker.py b/tools/esim_tool_manager/version_checker.py new file mode 100644 index 0000000000..4f267d3902 --- /dev/null +++ b/tools/esim_tool_manager/version_checker.py @@ -0,0 +1,108 @@ +import subprocess +from dataclasses import dataclass +from typing import Optional + +from .detector import ToolDetector + + +@dataclass +class VersionInfo: + tool: str + installed_version: Optional[str] + available_version: Optional[str] + + +class VersionChecker: + """Check installed and APT-available versions of eSim tools.""" + + PACKAGES = { + "ngspice": "ngspice", + "verilator": "verilator", + "ghdl": "ghdl", + "kicad": "kicad", + } + + def __init__(self) -> None: + self.detector = ToolDetector() + + def get_available_version( + self, + package: str, + ) -> Optional[str]: + """Get the candidate version available through APT.""" + + try: + result = subprocess.run( + ["apt-cache", "policy", package], + capture_output=True, + text=True, + timeout=10, + check=False, + ) + + for line in result.stdout.splitlines(): + if "Candidate:" in line: + version = line.split(":", 1)[1].strip() + + if version and version != "(none)": + return version + + return None + + except (subprocess.SubprocessError, OSError): + return None + + def check_tool(self, tool_name: str) -> VersionInfo: + """Get installed and available versions for one tool.""" + + tool_name = tool_name.lower() + + if tool_name not in self.PACKAGES: + raise ValueError( + f"Unsupported tool: {tool_name}" + ) + + status = self.detector.detect_tool( + tool_name, + self.PACKAGES[tool_name], + ) + + available_version = self.get_available_version( + self.PACKAGES[tool_name] + ) + + return VersionInfo( + tool=tool_name, + installed_version=status.version, + available_version=available_version, + ) + + def check_all(self) -> list[VersionInfo]: + """Check versions of all supported tools.""" + + return [ + self.check_tool(tool_name) + for tool_name in self.PACKAGES + ] + + +if __name__ == "__main__": + checker = VersionChecker() + + print("eSim Version Checker") + print("=" * 70) + + for info in checker.check_all(): + installed = info.installed_version or "Not installed" + available = info.available_version or "Not available" + + print(f"\n{info.tool}") + print(f" Installed : {installed}") + print(f" APT : {available}") + + if info.installed_version is None: + print(" Status : Not installed") + elif info.available_version is None: + print(" Status : Unable to check") + else: + print(" Status : Installed package detected")