333 lines
13 KiB
Python
333 lines
13 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()
|