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