SXXXXXXX_ProjectUtility/projectutility/__main__.py
2025-05-06 09:56:25 +02:00

263 lines
12 KiB
Python

# projectutility/__main__.py
import sys
import os
import logging
import argparse
import tkinter as tk
from tkinter import messagebox
import traceback
# --- Determine if Running as Frozen Executable (PyInstaller) ---
# getattr(sys, 'frozen', False) checks if the 'frozen' attribute exists (set by PyInstaller)
# sys._MEIPASS is the temporary directory where PyInstaller unpacks data in one-dir mode,
# or the path to the executable itself in one-file mode (we need the dir).
IS_FROZEN = getattr(sys, 'frozen', False)
# --- Calculate Base Paths ---
try:
if IS_FROZEN:
# Running as executable
# Base path is the directory containing the executable
# sys.executable is the path to the .exe file
REPO_ROOT = os.path.dirname(sys.executable)
# In the frozen state, the 'package' source isn't directly accessible in the same way.
# We assume the executable *is* the application root in this context.
# Files needed from the original package (like default config) would need
# to be bundled as data files by PyInstaller and accessed via sys._MEIPASS if needed.
# For now, assume generated dirs (logs, config) are relative to REPO_ROOT (executable dir).
APP_SOURCE_ROOT = REPO_ROOT # Or potentially sys._MEIPASS if accessing bundled package data
print(f"RUNNING FROZEN: REPO_ROOT={REPO_ROOT}, APP_SOURCE_ROOT={APP_SOURCE_ROOT}") # Debug print
else:
# Running as standard Python script (e.g., python -m ...)
APP_SOURCE_ROOT = os.path.dirname(os.path.abspath(__file__)) # .../projectutility
REPO_ROOT = os.path.dirname(APP_SOURCE_ROOT) # .../ProjectUtility
print(f"RUNNING SCRIPT: REPO_ROOT={REPO_ROOT}, APP_SOURCE_ROOT={APP_SOURCE_ROOT}") # Debug print
# Configure basic logging early
_log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
logging.basicConfig(level=logging.DEBUG, format=_log_format) # Configure early
logging.getLogger(__name__).info(f"Application running in {'FROZEN' if IS_FROZEN else 'SCRIPT'} mode.")
logging.getLogger(__name__).debug(f"Determined REPO_ROOT: {REPO_ROOT}")
logging.getLogger(__name__).debug(f"Determined APP_SOURCE_ROOT (contextual): {APP_SOURCE_ROOT}")
logging.getLogger(__name__).debug(f"Current sys.path: {sys.path}")
except Exception as e:
logging.getLogger(__name__).critical(
f"CRITICAL ERROR: Failed to determine application paths in __main__.py: {e}",
exc_info=True
)
try:
root = tk.Tk(); root.withdraw()
messagebox.showerror("Fatal Initialization Error", f"Could not determine critical application paths.\nError: {e}")
root.destroy()
except Exception: pass
sys.exit(1)
# --- Define Dynamic Paths ---
# Place generated files (logs, runtime config, state) relative to the executable when frozen,
# or inside the package source when running as script.
# Place tool directories relative to the executable/repo root.
if IS_FROZEN:
LOGS_DIR = os.path.join(REPO_ROOT, "logs")
CONFIG_DIR_MAIN = os.path.join(REPO_ROOT, "config") # Runtime config/state goes here
# TOOLS_DIR and MANAGED_TOOLS_DIR should also be relative to the executable's location
# These were likely already calculated correctly relative to REPO_ROOT in other modules if
# they also implement the IS_FROZEN check. We need to ensure consistency.
# Let's redefine them here for clarity in __main__ context for defaults.
TOOLS_DIR_MAIN = os.path.join(REPO_ROOT, "tools") # Local tools dir
MANAGED_TOOLS_DIR_MAIN = os.path.join(REPO_ROOT, "managed_tools") # Git tools dir
else:
LOGS_DIR = os.path.join(APP_SOURCE_ROOT, "logs") # Inside package src
CONFIG_DIR_MAIN = os.path.join(APP_SOURCE_ROOT, "config") # Inside package src
TOOLS_DIR_MAIN = os.path.join(REPO_ROOT, "tools") # In repo root
MANAGED_TOOLS_DIR_MAIN = os.path.join(REPO_ROOT, "managed_tools") # In repo root
# Default log file location
DEFAULT_LOG_FILE = os.path.join(LOGS_DIR, "project_utility.log")
DEFAULT_LOG_LEVEL = logging.INFO
# --- Import Internal Modules ---
# This import *should* now work when frozen if PyInstaller includes the package correctly.
try:
from projectutility.gui.main_window import MainWindow
# Import other necessary modules...
# Example: Ensure path calculation logic is imported/available if needed globally
# from projectutility.core import path_utils # Hypothetical module
# path_utils.set_frozen_state(IS_FROZEN, REPO_ROOT, APP_SOURCE_ROOT) # Pass state
logging.getLogger(__name__).info("Successfully imported MainWindow.")
except ImportError as e:
# This error now strongly suggests PyInstaller didn't bundle the package correctly.
error_message = f"ERROR: Failed to import ProjectUtility components in __main__.py.\n\n"
error_message += f"ImportError: {e}\n\n"
error_message += f"Mode: {'FROZEN' if IS_FROZEN else 'SCRIPT'}\n"
error_message += f"Python Executable: {sys.executable}\n"
error_message += f"sys.path:\n" + "\n".join(sys.path) + "\n\n"
if IS_FROZEN:
error_message += f"sys._MEIPASS (if exists): {getattr(sys, '_MEIPASS', 'Not Set')}\n\n"
error_message += "This usually means the 'projectutility' package was not correctly included by PyInstaller.\n"
error_message += "Check your .spec file or PyInstaller command (use --paths or pathex).\n"
else:
error_message += "Ensure you are running 'python -m projectutility' from the repository root directory ('ProjectUtility').\n"
error_message += "Also check if all '__init__.py' files exist in package directories."
print(error_message, file=sys.stderr) # Print to console
logging.getLogger(__name__).critical(error_message, exc_info=True)
traceback.print_exc()
try:
root = tk.Tk(); root.withdraw()
messagebox.showerror("Fatal Import Error", error_message)
root.destroy()
except Exception as tk_error:
logging.getLogger(__name__).error(f"Could not display Tkinter error message: {tk_error}")
sys.exit(1)
except Exception as e:
error_message = f"Unexpected error during imports in __main__.py: {e}"
print(error_message, file=sys.stderr)
logging.getLogger(__name__).critical(error_message, exc_info=True)
traceback.print_exc()
sys.exit(1)
# --- Logging Setup Function (Modified slightly for clarity) ---
def setup_logging(log_level: int, log_file: str) -> None:
"""Configures application logging to file and console."""
log_dir = os.path.dirname(log_file)
try:
os.makedirs(log_dir, exist_ok=True)
# Set permissions? Might be needed on some systems.
except OSError as e:
logging.getLogger(__name__).error(f"Could not create log directory '{log_dir}': {e}. Log file may not be written.")
# Continue without file logging? Or raise error? Let basicConfig handle FileHandler error.
log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
date_format = "%Y-%m-%d %H:%M:%S"
# Get root logger and remove existing handlers to prevent duplication
root_logger = logging.getLogger()
if root_logger.hasHandlers():
logging.getLogger(__name__).debug("Removing existing logging handlers before setup.")
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
handler.close()
# Setup new handlers
handlers = []
try:
file_handler = logging.FileHandler(log_file, mode='a', encoding='utf-8')
handlers.append(file_handler)
except Exception as e:
logging.getLogger(__name__).error(f"Failed to create FileHandler for '{log_file}': {e}. Logging to file disabled.")
handlers.append(logging.StreamHandler(sys.stdout)) # Always add console handler
logging.basicConfig(
level=log_level,
format=log_format,
datefmt=date_format,
handlers=handlers,
force=True # Override existing config (Python 3.8+)
)
logging.getLogger(__name__).info("-" * 50)
logging.getLogger(__name__).info(f"Logging configured. Level: {logging.getLevelName(log_level)}. File: {'Enabled' if file_handler in handlers else 'DISABLED'} ('{log_file}')")
logging.getLogger(__name__).info(f"Running Mode: {'FROZEN (Executable)' if IS_FROZEN else 'SCRIPT'}")
logging.getLogger(__name__).info(f"Base Directory (REPO_ROOT): {REPO_ROOT}")
# --- Argument Parsing Function (Unchanged) ---
def parse_arguments() -> argparse.Namespace:
"""Parses command-line arguments."""
parser = argparse.ArgumentParser(description="Project Utility Dashboard")
# Use the calculated defaults based on IS_FROZEN state
parser.add_argument(
"--log-level", type=str.upper, choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
default=logging.getLevelName(DEFAULT_LOG_LEVEL),
help=f"Set logging level (default: {logging.getLevelName(DEFAULT_LOG_LEVEL)})", metavar="LEVEL"
)
parser.add_argument(
"--log-file", type=str, default=DEFAULT_LOG_FILE,
help=f"Log file path (default: {DEFAULT_LOG_FILE})", metavar="FILEPATH"
)
# When frozen, sys.argv usually starts with the executable path.
# When run with `python -m`, it starts with the __main__.py path.
# Parsing sys.argv[1:] works correctly in both cases for user arguments.
args = parser.parse_args(sys.argv[1:])
logging.getLogger(__name__).debug(f"Command line arguments parsed: {args}")
return args
# --- Main Application Function (Small adjustments for logging paths) ---
def main() -> None:
"""Main function to initialize and run the application."""
# Parse args first to potentially override defaults
args = parse_arguments()
# Setup logging using potentially overridden paths/levels
log_level_int = logging.getLevelName(args.log_level)
setup_logging(log_level=log_level_int, log_file=args.log_file)
logging.info(f"Starting ProjectUtility application...")
# Log key paths determined earlier
logging.info(f"Repo Root (Base Path): {REPO_ROOT}")
logging.info(f"Config Directory: {CONFIG_DIR_MAIN}")
logging.info(f"Logs Directory: {LOGS_DIR}")
logging.info(f"Local Tools Directory: {TOOLS_DIR_MAIN}")
logging.info(f"Managed Tools Directory: {MANAGED_TOOLS_DIR_MAIN}")
root_tk: tk.Tk | None = None
main_app: MainWindow | None = None
try:
root_tk = tk.Tk()
root_tk.withdraw()
# Pass the necessary paths to MainWindow if it needs them explicitly
# Or ensure MainWindow calculates them correctly using the same IS_FROZEN logic
main_app = MainWindow(root_tk) # Assuming MainWindow handles its own path logic now
logging.info("Main application window initialized.")
except Exception as e:
logging.critical("CRITICAL ERROR: Failed to create the main application window.", exc_info=True)
try:
temp_root = tk.Tk(); temp_root.withdraw()
messagebox.showerror("Fatal Application Error", f"Failed to initialize the main window.\nError: {e}\nCheck logs.")
temp_root.destroy()
except Exception: pass
if root_tk and root_tk.winfo_exists(): root_tk.destroy()
sys.exit(1)
logging.info("Starting Tkinter main event loop.")
try:
if root_tk: root_tk.mainloop()
else: raise RuntimeError("Root window not created.")
except KeyboardInterrupt:
logging.info("Application interrupted by user (KeyboardInterrupt).")
if root_tk and main_app and hasattr(main_app, '_on_close'):
try: main_app._on_close()
except Exception as close_err:
logging.error(f"Error during _on_close after KeyboardInterrupt: {close_err}")
if root_tk.winfo_exists(): root_tk.destroy()
elif root_tk and root_tk.winfo_exists(): root_tk.destroy()
except Exception as loop_error:
logging.exception(f"An unexpected error occurred in the Tkinter main loop: {loop_error}")
if root_tk and root_tk.winfo_exists(): root_tk.destroy()
sys.exit(1)
finally:
logging.debug("Tkinter main loop ended.")
logging.info("ProjectUtility application finished.")
sys.exit(0)
# --- Main Execution Block ---
if __name__ == "__main__":
# Add debug prints for paths when run directly
print(f"Running __main__ directly. IS_FROZEN={IS_FROZEN}")
print(f"REPO_ROOT={REPO_ROOT}")
print(f"APP_SOURCE_ROOT={APP_SOURCE_ROOT}")
print(f"CONFIG_DIR_MAIN={CONFIG_DIR_MAIN}")
print(f"LOGS_DIR={LOGS_DIR}")
print(f"DEFAULT_LOG_FILE={DEFAULT_LOG_FILE}")
main()