Completely refactored gresource to correspond SOLID principles

- Create logger interface and updated its usage by console.py
This commit is contained in:
Vladyslav Hroshev
2025-04-06 17:02:49 +03:00
parent 8da9b564be
commit 9bb229df7d
17 changed files with 316 additions and 182 deletions

View File

@@ -6,9 +6,10 @@ from .theme import Theme
from .utils import remove_properties, remove_keywords
from . import config
from .utils.alternatives_updater import AlternativesUpdater
from .utils.console import Console, Color, Format
from scripts.utils.logger.console import Console, Color, Format
from .utils.files_labeler import FilesLabeler
from .utils.gresource import Gresource, GresourceBackupNotFoundError
from .utils.gresource import GresourceBackupNotFoundError
from .utils.gresource.gresource import Gresource
class GlobalTheme:
@@ -40,7 +41,7 @@ class GlobalTheme:
self.__gresource_file = os.path.join(self.destination_folder, self.destination_file)
self.__gresource_temp_folder = os.path.join(self.temp_folder, config.extracted_gdm_folder)
self.__gresource = Gresource(self.destination_file, self.__gresource_temp_folder, self.destination_folder)
self.__gresource = Gresource(self.destination_file, self.__gresource_temp_folder, self.destination_folder, logger_factory=Console())
def prepare(self):
if self.__is_installed():

View File

@@ -3,7 +3,7 @@ import os
from scripts import config
from scripts.gdm import GlobalTheme
from scripts.install.theme_installer import ThemeInstaller
from scripts.utils.console import Console, Color, Format
from scripts.utils.logger.console import Console, Color, Format
class GlobalThemeInstaller(ThemeInstaller):

View File

@@ -4,7 +4,7 @@ from scripts import config
from scripts.install.theme_installer import ThemeInstaller
from scripts.theme import Theme
from scripts.utils import remove_files
from scripts.utils.console import Console, Color, Format
from scripts.utils.logger.console import Console, Color, Format
class LocalThemeInstaller(ThemeInstaller):

View File

@@ -8,7 +8,7 @@ from .utils import (
copy_files, # copy files from source to destination
destination_return, # copied/modified theme location
generate_file) # combine files from folder to one file
from .utils.console import Console, Color, Format
from scripts.utils.logger.console import Console, Color, Format
class Theme:

View File

@@ -2,7 +2,7 @@ import functools
import subprocess
from typing import TypeAlias
from scripts.utils.console import Console
from scripts.utils.logger.console import Console
PathString: TypeAlias = str | bytes

View File

@@ -2,7 +2,7 @@ import subprocess
import time
from scripts import config
from scripts.utils.console import Console, Format, Color
from scripts.utils.logger.console import Console, Format, Color
from scripts.utils.parse_folder import parse_folder

View File

@@ -1,171 +0,0 @@
import os
import subprocess
import textwrap
from pathlib import Path
from scripts.utils.console import Console
class GresourceBackupNotFoundError(FileNotFoundError):
def __init__(self, location: str = None):
if location:
super().__init__(f"Gresource backup file not found: {location}")
else:
super().__init__("Gresource backup file not found.")
class MissingDependencyError(Exception):
def __init__(self, dependency: str):
super().__init__(f"Missing required dependency: {dependency}")
self.dependency = dependency
class Gresource:
"""Handles the extraction and compilation of gresource files for GNOME Shell themes."""
def __init__(self, gresource_file: str, temp_folder: str, destination: str):
"""
:param gresource_file: The name of the gresource file to be processed.
:param temp_folder: The temporary folder where resources will be extracted.
:param destination: The destination folder where the compiled gresource file will be saved.
"""
self.gresource_file = gresource_file
self.temp_folder = temp_folder
self.destination = destination
self._temp_gresource = os.path.join(temp_folder, gresource_file)
self._destination_gresource = os.path.join(destination, gresource_file)
self._active_source_gresource = self._destination_gresource
self._backup_gresource = os.path.join(destination, f"{gresource_file}.backup")
self._gresource_xml = os.path.join(temp_folder, f"{gresource_file}.xml")
def use_backup_gresource(self):
if not os.path.exists(self._backup_gresource):
raise GresourceBackupNotFoundError(self._backup_gresource)
self._active_source_gresource = self._backup_gresource
def extract(self):
extract_line = Console.Line()
extract_line.update("Extracting gresource files...")
resources = self._get_resources_list()
self._extract_resources(resources)
extract_line.success("Extracted gresource files.")
def _get_resources_list(self):
resources_list_response = subprocess.run(
["gresource", "list", self._active_source_gresource],
capture_output=True, text=True, check=False
)
if resources_list_response.stderr:
raise Exception(f"gresource could not process the theme file: {self._active_source_gresource}")
return resources_list_response.stdout.strip().split("\n")
def _extract_resources(self, resources: list[str]):
prefix = "/org/gnome/shell/theme/"
try:
for resource in resources:
resource_path = resource.replace(prefix, "")
output_path = os.path.join(self.temp_folder, resource_path)
os.makedirs(os.path.dirname(output_path), exist_ok=True)
with open(output_path, 'wb') as f:
subprocess.run(
["gresource", "extract", self._active_source_gresource, resource],
stdout=f, check=True
)
except FileNotFoundError as e:
if "gresource" in str(e):
self._raise_gresource_error(e)
raise
@staticmethod
def _raise_gresource_error(e: Exception):
print("Error: 'gresource' command not found.")
print("Please install the glib2-devel package:")
print(" - For Fedora/RHEL: sudo dnf install glib2-devel")
print(" - For Ubuntu/Debian: sudo apt install libglib2.0-dev")
print(" - For Arch: sudo pacman -S glib2-devel")
raise MissingDependencyError("glib2-devel") from e
def compile(self):
compile_line = Console.Line()
compile_line.update("Compiling gnome-shell theme...")
self._create_gresource_xml()
self._compile_resources()
compile_line.success("Theme compiled.")
def _create_gresource_xml(self):
with open(self._gresource_xml, 'w') as gresource_xml:
gresource_xml.write(self._generate_gresource_xml())
def _generate_gresource_xml(self):
files_to_include = self._get_files_to_include()
nl = "\n" # fstring doesn't support newline character
return textwrap.dedent(f"""
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/org/gnome/shell/theme">
{nl.join(files_to_include)}
</gresource>
</gresources>
""")
def _get_files_to_include(self):
temp_path = Path(self.temp_folder)
return [
f"<file>{file.relative_to(temp_path)}</file>"
for file in temp_path.glob('**/*')
if file.is_file()
]
def _compile_resources(self):
try:
subprocess.run(["glib-compile-resources",
"--sourcedir", self.temp_folder,
"--target", self._temp_gresource,
self._gresource_xml
],
cwd=self.temp_folder, check=True)
except FileNotFoundError as e:
if "glib-compile-resources" in str(e):
self._raise_gresource_error(e)
raise
def backup(self):
backup_line = Console.Line()
backup_line.update("Backing up gresource files...")
subprocess.run(["cp", "-aT",
self._destination_gresource,
self._backup_gresource],
check=True)
backup_line.success("Backed up gresource files.")
def restore(self):
if not os.path.exists(self._backup_gresource):
raise GresourceBackupNotFoundError(self._backup_gresource)
subprocess.run(["mv", "-f",
self._backup_gresource,
self._destination_gresource],
check=True)
def move(self):
move_line = Console.Line()
move_line.update("Moving gresource files...")
subprocess.run(["cp", "-f",
self._temp_gresource,
self._destination_gresource],
check=True)
subprocess.run(["chmod", "644", self._destination_gresource], check=True)
move_line.success("Moved gresource files.")

View File

@@ -0,0 +1,21 @@
class GresourceBackupNotFoundError(FileNotFoundError):
def __init__(self, location: str = None):
if location:
super().__init__(f"Gresource backup file not found: {location}")
else:
super().__init__("Gresource backup file not found.")
class MissingDependencyError(Exception):
def __init__(self, dependency: str):
super().__init__(f"Missing required dependency: {dependency}")
self.dependency = dependency
def raise_gresource_error(tool: str, e: Exception):
print(f"Error: '{tool}' command not found.")
print("Please install the glib2-devel package:")
print(" - For Fedora/RHEL: sudo dnf install glib2-devel")
print(" - For Ubuntu/Debian: sudo apt install libglib2.0-dev")
print(" - For Arch: sudo pacman -S glib2-devel")
raise MissingDependencyError("glib2-devel") from e

View File

@@ -0,0 +1,46 @@
import os
from scripts.utils.gresource.gresource_backuper import GresourceBackuperManager
from scripts.utils.gresource.gresource_complier import GresourceCompiler
from scripts.utils.gresource.gresource_extractor import GresourceExtractor
from scripts.utils.gresource.gresource_mover import GresourceMover
from scripts.utils.logger.logger import LoggerFactory
class Gresource:
"""Orchestrator for gresource files. Manages the extraction, compilation, and backup of gresource files."""
def __init__(self, gresource_file: str, temp_folder: str, destination: str, logger_factory: LoggerFactory):
"""
:param gresource_file: The name of the gresource file to be processed.
:param temp_folder: The temporary folder where resources will be extracted.
:param destination: The destination folder where the compiled gresource file will be saved.
"""
self.gresource_file = gresource_file
self.temp_folder = temp_folder
self.destination = destination
self.logger_factory = logger_factory
self._temp_gresource = os.path.join(temp_folder, gresource_file)
self._destination_gresource = os.path.join(destination, gresource_file)
self._active_source_gresource = self._destination_gresource
self._backuper = GresourceBackuperManager(self._destination_gresource, logger_factory=self.logger_factory)
def use_backup_gresource(self):
self._active_source_gresource = self._backuper.get_backup()
def extract(self):
GresourceExtractor(self._active_source_gresource, self.temp_folder, logger_factory=self.logger_factory).extract()
def compile(self):
GresourceCompiler(self.temp_folder, self._temp_gresource, logger_factory=self.logger_factory).compile()
def backup(self):
self._backuper.backup()
def restore(self):
self._backuper.restore()
def move(self):
GresourceMover(self._temp_gresource, self._destination_gresource, logger_factory=self.logger_factory).move()

View File

@@ -0,0 +1,52 @@
import os
import shutil
import subprocess
from scripts.utils.gresource import GresourceBackupNotFoundError
from scripts.utils.logger.logger import LoggerFactory
class GresourceBackuperManager:
def __init__(self, destination_file: str, logger_factory: LoggerFactory):
self.destination_file = destination_file
self._backup_file = f"{destination_file}.backup"
self._backuper = GresourceBackuper(destination_file, self._backup_file, logger_factory)
def backup(self):
self._backuper.backup()
def restore(self):
self._backuper.restore()
def get_backup(self) -> str:
return self._backuper.get_backup()
class GresourceBackuper:
def __init__(self, destination_file: str, backup_file, logger_factory: LoggerFactory):
self.destination_file = destination_file
self.backup_file = backup_file
self.logger_factory = logger_factory
def get_backup(self) -> str:
if not os.path.exists(self.backup_file):
raise GresourceBackupNotFoundError(self.backup_file)
return self.backup_file
def backup(self):
backup_line = self.logger_factory.create_logger()
backup_line.update("Backing up gresource files...")
if os.path.exists(self.backup_file):
os.remove(self.backup_file)
shutil.copy2(self.destination_file, self.backup_file)
# subprocess.run(["cp", "-aT", self.destination_file, self.backup_file], check=True)
backup_line.success("Backed up gresource files.")
def restore(self):
if not os.path.exists(self.backup_file):
raise GresourceBackupNotFoundError(self.backup_file)
subprocess.run(["mv", "-f", self.backup_file, self.destination_file], check=True)

View File

@@ -0,0 +1,63 @@
import subprocess
import textwrap
from pathlib import Path
from scripts.utils.gresource import raise_gresource_error
from scripts.utils.logger.logger import LoggerFactory
class GresourceCompiler:
def __init__(self, source_folder: str, target_file: str, logger_factory: LoggerFactory):
self.source_folder = source_folder
self.target_file = target_file
self.gresource_xml = target_file + ".xml"
self.logger_factory = logger_factory
def compile(self):
compile_line = self.logger_factory.create_logger()
compile_line.update("Compiling gnome-shell theme...")
self._create_gresource_xml()
self._compile_resources()
compile_line.success("Compiled gnome-shell theme.")
def _create_gresource_xml(self):
with open(self.gresource_xml, 'w') as gresource_xml:
gresource_xml.write(self._generate_gresource_xml())
def _generate_gresource_xml(self):
files_to_include = self._get_files_to_include()
nl = "\n" # fstring doesn't support newline character
return textwrap.dedent(f"""
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/org/gnome/shell/theme">
{nl.join(files_to_include)}
</gresource>
</gresources>
""")
def _get_files_to_include(self):
source_path = Path(self.source_folder)
return [
f"<file>{file.relative_to(source_path)}</file>"
for file in source_path.glob('**/*')
if file.is_file()
]
def _compile_resources(self):
try:
self._try_compile_resources()
except FileNotFoundError as e:
if "glib-compile-resources" in str(e):
raise_gresource_error("glib-compile-resources", e)
raise
def _try_compile_resources(self):
subprocess.run(["glib-compile-resources",
"--sourcedir", self.source_folder,
"--target", self.target_file,
self.gresource_xml
],
cwd=self.source_folder, check=True)

View File

@@ -0,0 +1,53 @@
import os
import subprocess
from scripts.utils.gresource import raise_gresource_error
from scripts.utils.logger.logger import LoggerFactory
class GresourceExtractor:
def __init__(self, gresource_path: str, extract_folder: str, logger_factory: LoggerFactory):
self.gresource_path = gresource_path
self.extract_folder = extract_folder
self.logger_factory = logger_factory
def extract(self):
extract_line = self.logger_factory.create_logger()
extract_line.update("Extracting gresource files...")
resources = self._get_resources_list()
self._extract_resources(resources)
extract_line.success("Extracted gresource files.")
def _get_resources_list(self):
resources_list_response = subprocess.run(
["gresource", "list", self.gresource_path],
capture_output=True, text=True, check=False
)
if resources_list_response.stderr:
raise Exception(f"gresource could not process the theme file: {self.gresource_path}")
return resources_list_response.stdout.strip().split("\n")
def _extract_resources(self, resources: list[str]):
try:
self._try_extract_resources(resources)
except FileNotFoundError as e:
if "gresource" in str(e):
raise_gresource_error("gresource", e)
raise
def _try_extract_resources(self, resources: list[str]):
prefix = "/org/gnome/shell/theme/"
for resource in resources:
resource_path = resource.replace(prefix, "")
output_path = os.path.join(self.extract_folder, resource_path)
os.makedirs(os.path.dirname(output_path), exist_ok=True)
with open(output_path, 'wb') as f:
subprocess.run(
["gresource", "extract", self.gresource_path, resource],
stdout=f, check=True
)

View File

@@ -0,0 +1,23 @@
import subprocess
from scripts.utils.logger.logger import LoggerFactory
class GresourceMover:
def __init__(self, source_file: str, destination_file: str, logger_factory: LoggerFactory):
self.source_file = source_file
self.destination_file = destination_file
self.logger_factory = logger_factory
def move(self):
move_line = self.logger_factory.create_logger()
move_line.update("Moving gresource files...")
subprocess.run(["cp", "-f",
self.source_file,
self.destination_file],
check=True)
subprocess.run(["chmod", "644", self.destination_file], check=True)
move_line.success("Moved gresource files.")

View File

View File

@@ -3,14 +3,24 @@ import threading
from enum import Enum
from typing import Optional
from scripts.utils.logger.logger import LoggerFactory, Logger
class Console:
class Console(LoggerFactory):
"""Manages console output for concurrent processes with line tracking"""
_print_lock = threading.Lock()
_line_mapping = {}
_next_line = 0
class Line:
def create_logger(self, name: Optional[str]=None) -> 'Console.Line':
"""
Create a logger instance with the given name.
:param name: Name of the logger.
:return: Logger instance.
"""
return Console.Line(name)
class Line(Logger):
def __init__(self, name: Optional[str]=None):
"""Initialize a new managed line"""
self.name = name or f"line_{Console._next_line}"

View File

@@ -0,0 +1,36 @@
from abc import ABC, abstractmethod
from typing import Optional
class LoggerFactory(ABC):
@staticmethod
@abstractmethod
def create_logger(name: Optional[str] = None) -> 'Logger':
"""
Create a logger instance with the given name.
:param name: Name of the logger.
:return: Logger instance.
"""
pass
class Logger(ABC):
@abstractmethod
def update(self, message: str):
pass
@abstractmethod
def success(self, message):
pass
@abstractmethod
def error(self, message):
pass
@abstractmethod
def warn(self, message):
pass
@abstractmethod
def info(self, message):
pass

View File

@@ -7,7 +7,7 @@ import shutil
from collections import defaultdict
from typing import Any
from .console import Console, Color, Format
from scripts.utils.logger.console import Console, Color, Format
from .parse_folder import parse_folder
from .. import config
import os