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 .utils import remove_properties, remove_keywords
from . import config from . import config
from .utils.alternatives_updater import AlternativesUpdater 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.files_labeler import FilesLabeler
from .utils.gresource import Gresource, GresourceBackupNotFoundError from .utils.gresource import GresourceBackupNotFoundError
from .utils.gresource.gresource import Gresource
class GlobalTheme: class GlobalTheme:
@@ -40,7 +41,7 @@ class GlobalTheme:
self.__gresource_file = os.path.join(self.destination_folder, self.destination_file) 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_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): def prepare(self):
if self.__is_installed(): if self.__is_installed():

View File

@@ -3,7 +3,7 @@ import os
from scripts import config from scripts import config
from scripts.gdm import GlobalTheme from scripts.gdm import GlobalTheme
from scripts.install.theme_installer import ThemeInstaller 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): class GlobalThemeInstaller(ThemeInstaller):

View File

@@ -4,7 +4,7 @@ from scripts import config
from scripts.install.theme_installer import ThemeInstaller from scripts.install.theme_installer import ThemeInstaller
from scripts.theme import Theme from scripts.theme import Theme
from scripts.utils import remove_files 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): class LocalThemeInstaller(ThemeInstaller):

View File

@@ -8,7 +8,7 @@ from .utils import (
copy_files, # copy files from source to destination copy_files, # copy files from source to destination
destination_return, # copied/modified theme location destination_return, # copied/modified theme location
generate_file) # combine files from folder to one file 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: class Theme:

View File

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

View File

@@ -2,7 +2,7 @@ import subprocess
import time import time
from scripts import config 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 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 enum import Enum
from typing import Optional 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""" """Manages console output for concurrent processes with line tracking"""
_print_lock = threading.Lock() _print_lock = threading.Lock()
_line_mapping = {} _line_mapping = {}
_next_line = 0 _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): def __init__(self, name: Optional[str]=None):
"""Initialize a new managed line""" """Initialize a new managed line"""
self.name = name or f"line_{Console._next_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 collections import defaultdict
from typing import Any 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 .parse_folder import parse_folder
from .. import config from .. import config
import os import os