Initial gradle-mirror CLI project

This commit is contained in:
2026-04-08 12:33:06 +03:30
commit 1e6b55d20b
12 changed files with 589 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
"""gradle_mirror package."""
__all__ = ["__version__"]
__version__ = "1.0.0"

52
gradle_mirror/cli.py Normal file
View File

@@ -0,0 +1,52 @@
"""Typer CLI entrypoint for gradle-mirror."""
from __future__ import annotations
import typer
from .gradle_locator import detect_gradle_user_home
from .mirror_manager import MirrorManager
from .utils import setup_logger
app = typer.Typer(
add_completion=False,
no_args_is_help=True,
help="Activate/deactivate Gradle mirror repositories via init.gradle.kts.",
)
def _build_manager() -> MirrorManager:
gradle_home = detect_gradle_user_home()
logger = setup_logger(gradle_home)
return MirrorManager(gradle_home=gradle_home, logger=logger)
@app.command()
def active() -> None:
"""Activate global mirror configuration."""
try:
manager = _build_manager()
result = manager.activate()
typer.secho(f"{result.message}", fg=typer.colors.GREEN)
typer.secho(f"📁 Updated {result.init_file}", fg=typer.colors.BRIGHT_BLUE)
except Exception as exc: # pragma: no cover - top-level safety
typer.secho(f"❌ Failed to activate mirror: {exc}", fg=typer.colors.RED, err=True)
raise typer.Exit(code=1) from exc
@app.command()
def deactive() -> None:
"""Deactivate mirror configuration."""
try:
manager = _build_manager()
result = manager.deactivate()
color = typer.colors.YELLOW if not result.changed else typer.colors.GREEN
emoji = "" if not result.changed else ""
typer.secho(f"{emoji} {result.message}", fg=color)
if result.init_file.exists():
typer.secho(f"📁 Current file {result.init_file}", fg=typer.colors.BRIGHT_BLUE)
except Exception as exc: # pragma: no cover - top-level safety
typer.secho(f"❌ Failed to deactivate mirror: {exc}", fg=typer.colors.RED, err=True)
raise typer.Exit(code=1) from exc

View File

@@ -0,0 +1,22 @@
"""Application constants used by gradle_mirror."""
from __future__ import annotations
from pathlib import Path
APP_NAME = "gradle-mirror"
INIT_FILE_NAME = "init.gradle.kts"
BACKUP_SUFFIX = ".bak"
LOG_FILE_NAME = "gradle-mirror.log"
START_MARKER = "/* gradle-mirror-start */"
END_MARKER = "/* gradle-mirror-end */"
MIRROR_REPOSITORIES = [
"https://maven.myket.ir/",
"https://mirror-maven.runflare.com/maven2/",
"https://mirror-maven.runflare.com/android/maven2/",
"https://mirror-maven.runflare.com/gradle-plugins/",
]
DEFAULT_GRADLE_HOME = Path.home() / ".gradle"

View File

@@ -0,0 +1,21 @@
"""Gradle home detection utilities."""
from __future__ import annotations
import os
from pathlib import Path
from .constants import DEFAULT_GRADLE_HOME
def detect_gradle_user_home() -> Path:
"""Return Gradle user home using env override when set.
Gradle honors ``GRADLE_USER_HOME``. If it is absent, default to ``~/.gradle``.
"""
env_home = os.getenv("GRADLE_USER_HOME")
if env_home:
return Path(env_home).expanduser().resolve()
return DEFAULT_GRADLE_HOME

8
gradle_mirror/init.py Normal file
View File

@@ -0,0 +1,8 @@
"""Compatibility module requested by project structure.
Use ``gradle_mirror.__init__`` for package metadata.
"""
from .__init__ import __version__
__all__ = ["__version__"]

9
gradle_mirror/main.py Normal file
View File

@@ -0,0 +1,9 @@
"""Executable module for `python -m gradle_mirror.main`."""
from __future__ import annotations
from .cli import app
if __name__ == "__main__":
app()

View File

@@ -0,0 +1,176 @@
"""Core business logic for activating/deactivating Gradle mirrors."""
from __future__ import annotations
import logging
import re
from dataclasses import dataclass
from pathlib import Path
from .constants import (
BACKUP_SUFFIX,
END_MARKER,
INIT_FILE_NAME,
START_MARKER,
)
from .utils import read_text_if_exists, write_text
@dataclass(frozen=True)
class OperationResult:
"""Result object returned by mirror operations."""
changed: bool
message: str
init_file: Path
backup_file: Path
class MirrorManager:
"""Manage insertion/removal of mirror repositories in init.gradle.kts."""
def __init__(self, gradle_home: Path, logger: logging.Logger) -> None:
self.gradle_home = gradle_home
self.logger = logger
self.init_file = self.gradle_home / INIT_FILE_NAME
self.backup_file = self.gradle_home / f"{INIT_FILE_NAME}{BACKUP_SUFFIX}"
def activate(self) -> OperationResult:
"""Activate global repository mirrors via init.gradle.kts."""
self.gradle_home.mkdir(parents=True, exist_ok=True)
existing = read_text_if_exists(self.init_file)
if self.init_file.exists():
# Keep the original state so deactivation can restore it exactly.
write_text(self.backup_file, existing)
self.logger.info("Created backup at %s", self.backup_file)
block = self._build_marked_block()
updated = self._upsert_marked_block(existing, block)
if updated == existing:
self.logger.info("Mirror block already up-to-date: %s", self.init_file)
return OperationResult(
changed=False,
message="Gradle mirror is already active",
init_file=self.init_file,
backup_file=self.backup_file,
)
write_text(self.init_file, updated)
self.logger.info("Activated mirrors in %s", self.init_file)
return OperationResult(
changed=True,
message="Gradle mirror activated",
init_file=self.init_file,
backup_file=self.backup_file,
)
def deactivate(self) -> OperationResult:
"""Deactivate mirror configuration and restore original behavior."""
if self.backup_file.exists():
backup_content = read_text_if_exists(self.backup_file)
if backup_content.strip():
write_text(self.init_file, backup_content)
self.logger.info("Restored init script from backup %s", self.backup_file)
else:
if self.init_file.exists():
self.init_file.unlink()
self.logger.info("Deleted empty init script %s", self.init_file)
self.backup_file.unlink(missing_ok=True)
self.logger.info("Removed backup file %s", self.backup_file)
return OperationResult(
changed=True,
message="Gradle mirror deactivated (restored from backup)",
init_file=self.init_file,
backup_file=self.backup_file,
)
if not self.init_file.exists():
self.logger.info("Nothing to deactivate; %s does not exist", self.init_file)
return OperationResult(
changed=False,
message="Nothing to deactivate",
init_file=self.init_file,
backup_file=self.backup_file,
)
current = read_text_if_exists(self.init_file)
updated = self._remove_marked_block(current)
if updated == current:
self.logger.info("No mirror markers found in %s", self.init_file)
return OperationResult(
changed=False,
message="Mirror markers were not found",
init_file=self.init_file,
backup_file=self.backup_file,
)
if updated.strip():
write_text(self.init_file, updated)
self.logger.info("Removed mirror block from %s", self.init_file)
else:
self.init_file.unlink(missing_ok=True)
self.logger.info("Removed empty init script %s", self.init_file)
return OperationResult(
changed=True,
message="Gradle mirror deactivated",
init_file=self.init_file,
backup_file=self.backup_file,
)
def _upsert_marked_block(self, content: str, marked_block: str) -> str:
pattern = self._marker_pattern()
if pattern.search(content):
return pattern.sub(marked_block, content)
base = content.rstrip()
if base:
return f"{base}\n\n{marked_block}\n"
return f"{marked_block}\n"
def _remove_marked_block(self, content: str) -> str:
pattern = self._marker_pattern()
removed = pattern.sub("", content)
# Normalize repeated blank lines left by block removal.
removed = re.sub(r"\n{3,}", "\n\n", removed)
return removed.strip() + ("\n" if removed.strip() else "")
def _marker_pattern(self) -> re.Pattern[str]:
escaped_start = re.escape(START_MARKER)
escaped_end = re.escape(END_MARKER)
return re.compile(rf"{escaped_start}[\s\S]*?{escaped_end}", re.MULTILINE)
def _build_marked_block(self) -> str:
script_body = """allprojects {
repositories {
clear()
maven { url = uri(\"https://maven.myket.ir/\") }
maven { url = uri(\"https://mirror-maven.runflare.com/maven2/\") }
maven { url = uri(\"https://mirror-maven.runflare.com/android/maven2/\") }
maven { url = uri(\"https://mirror-maven.runflare.com/gradle-plugins/\") }
mavenCentral()
google()
}
}
gradle.settingsEvaluated {
settings.pluginManagement.repositories.apply {
clear()
maven { url = uri(\"https://maven.myket.ir/\") }
maven { url = uri(\"https://mirror-maven.runflare.com/gradle-plugins/\") }
mavenCentral()
google()
}
}"""
return f"{START_MARKER}\n{script_body}\n{END_MARKER}"

50
gradle_mirror/utils.py Normal file
View File

@@ -0,0 +1,50 @@
"""General utility helpers for file operations and logging."""
from __future__ import annotations
import logging
from pathlib import Path
from .constants import APP_NAME, LOG_FILE_NAME
def setup_logger(gradle_home: Path) -> logging.Logger:
"""Configure and return the application logger.
Logs are written to ``<gradle_home>/gradle-mirror.log`` and include timestamps.
"""
gradle_home.mkdir(parents=True, exist_ok=True)
log_path = gradle_home / LOG_FILE_NAME
logger = logging.getLogger(APP_NAME)
if logger.handlers:
return logger
logger.setLevel(logging.INFO)
formatter = logging.Formatter(
fmt="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
file_handler = logging.FileHandler(log_path, encoding="utf-8")
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
logger.propagate = False
return logger
def read_text_if_exists(path: Path) -> str:
"""Read UTF-8 text if file exists; otherwise return empty string."""
if not path.exists():
return ""
return path.read_text(encoding="utf-8")
def write_text(path: Path, content: str) -> None:
"""Write UTF-8 text content, creating parent folders if needed."""
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")