mirror of
https://github.com/imayushsaini/Bombsquad-Ballistica-Modded-Server.git
synced 2026-08-22 13:13:23 +00:00
247 lines
8.4 KiB
Python
247 lines
8.4 KiB
Python
# Released under the MIT License. See LICENSE for details.
|
|
#
|
|
"""Apply small text patches to packages installed in a venv.
|
|
|
|
Some Python projects need targeted modifications to third-party
|
|
packages installed via ``pip`` — bug fixes pending upstream review,
|
|
small project-specific customizations, or workarounds for
|
|
upstream-rejected behaviour. This module provides a simple,
|
|
declarative way to manage such patches.
|
|
|
|
Patches are described in a JSON file (commonly
|
|
``pconfig/venv_patches.json``) using the :class:`VenvPatches` schema
|
|
below. Each entry is a literal-string find-and-replace, validated
|
|
by an exact occurrence count so silent drift across upstream
|
|
versions surfaces immediately as a hard error.
|
|
|
|
Two operations are supported:
|
|
|
|
* :func:`apply_patches` — typically called once at the end of venv
|
|
construction (after ``pip install``). Replaces ``srctxt`` with
|
|
``replacetxt`` in each target file.
|
|
|
|
* :func:`check_patches` — verifies all patches are present in the
|
|
current venv. Designed to be called at app startup so a venv that
|
|
was assembled without the patches (e.g. an installer that ran
|
|
with ``allow_mismatches=True`` and skipped a broken patch) gets
|
|
flagged via a CRITICAL log entry.
|
|
|
|
The string-search approach (rather than SHA256 of full files or
|
|
unified-diff matching) is intentional: small targeted patches stay
|
|
robust as upstream files evolve around them, as long as the
|
|
specific lines we touch don't change.
|
|
"""
|
|
|
|
import logging
|
|
import sysconfig
|
|
from pathlib import Path
|
|
from dataclasses import dataclass, field
|
|
from typing import Annotated, TYPE_CHECKING
|
|
|
|
from efro.dataclassio import ioprepped, IOAttrs, dataclass_from_json
|
|
from efro.error import CleanError
|
|
|
|
if TYPE_CHECKING:
|
|
pass
|
|
|
|
|
|
_log = logging.getLogger(__name__)
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class VenvPatch:
|
|
"""A single string-replacement patch against a venv site-packages file."""
|
|
|
|
#: Short human-readable identifier. Used in log/error messages so
|
|
#: a failure points at the right patch.
|
|
description: Annotated[str, IOAttrs('description')]
|
|
|
|
#: Path to the target file relative to ``site-packages`` (e.g.
|
|
#: ``google/cloud/firestore_v1/watch.py``).
|
|
path: Annotated[str, IOAttrs('path')]
|
|
|
|
#: Exact text to find. Must appear ``count`` times in the target.
|
|
srctxt: Annotated[str, IOAttrs('srctxt')]
|
|
|
|
#: Text to replace ``srctxt`` with.
|
|
replacetxt: Annotated[str, IOAttrs('replacetxt')]
|
|
|
|
#: Number of expected occurrences of ``srctxt``. Defaults to 1.
|
|
#: When ``> 1``, every occurrence is replaced (so all become
|
|
#: ``replacetxt``). To replace different occurrences with
|
|
#: different text, split into separate patch entries with
|
|
#: distinguishing surrounding context in ``srctxt``.
|
|
count: Annotated[int, IOAttrs('count', soft_default=1)] = 1
|
|
|
|
|
|
@ioprepped
|
|
@dataclass
|
|
class VenvPatches:
|
|
"""Top-level container for a list of venv patches.
|
|
|
|
This is the schema for ``pconfig/venv_patches.json``.
|
|
"""
|
|
|
|
patches: Annotated[list[VenvPatch], IOAttrs('patches')] = field(
|
|
default_factory=list
|
|
)
|
|
|
|
|
|
def _validate(patches: list[VenvPatch]) -> None:
|
|
"""Validate inter-patch invariants.
|
|
|
|
Currently checks that ``srctxt`` is non-empty and does not appear
|
|
inside its own ``replacetxt`` (which would cause re-application
|
|
on every run to grow indefinitely). Also disallows ``count <= 0``.
|
|
"""
|
|
for p in patches:
|
|
if not p.srctxt:
|
|
raise CleanError(
|
|
f'venv-patch {p.description!r}: srctxt must be non-empty.'
|
|
)
|
|
if p.count <= 0:
|
|
raise CleanError(
|
|
f'venv-patch {p.description!r}:'
|
|
f' count must be positive (got {p.count}).'
|
|
)
|
|
if p.srctxt in p.replacetxt:
|
|
raise CleanError(
|
|
f'venv-patch {p.description!r}: srctxt appears'
|
|
' inside replacetxt; this would cause re-application'
|
|
' to grow without bound. Add disambiguating context'
|
|
' to srctxt or restructure replacetxt.'
|
|
)
|
|
|
|
|
|
def _site_packages() -> Path:
|
|
"""Return the site-packages dir of the current Python interpreter."""
|
|
purelib = sysconfig.get_paths().get('purelib')
|
|
if not purelib:
|
|
raise CleanError(
|
|
'Could not determine site-packages location from sysconfig.'
|
|
)
|
|
return Path(purelib)
|
|
|
|
|
|
def load_patches_from_file(path: str | Path) -> VenvPatches:
|
|
"""Load and validate a :class:`VenvPatches` JSON file."""
|
|
src = Path(path).read_text(encoding='utf-8')
|
|
out: VenvPatches = dataclass_from_json(VenvPatches, src)
|
|
_validate(out.patches)
|
|
return out
|
|
|
|
|
|
def apply_patches(
|
|
patches: list[VenvPatch],
|
|
*,
|
|
site_packages: Path | None = None,
|
|
allow_mismatches: bool = False,
|
|
) -> int:
|
|
"""Apply each patch in order to the current venv's site-packages.
|
|
|
|
Args:
|
|
patches: Patches to apply.
|
|
site_packages: Optional override. If ``None``, uses the
|
|
current interpreter's ``site-packages`` directory.
|
|
allow_mismatches: If ``True``, mismatched or missing patches
|
|
are logged at ERROR level and skipped instead of raising.
|
|
Use this during installer flows on production nodes where
|
|
a partial venv is preferable to a broken boot. The default
|
|
``False`` is the right choice for dev ``make env`` flows.
|
|
|
|
Returns the number of patches that were either successfully
|
|
applied or already present (i.e. the number that ended in the
|
|
desired state). Errors/skips are not counted.
|
|
"""
|
|
if site_packages is None:
|
|
site_packages = _site_packages()
|
|
succeeded = 0
|
|
for p in patches:
|
|
target = site_packages / p.path
|
|
try:
|
|
_apply_one(target, p)
|
|
succeeded += 1
|
|
except CleanError as exc:
|
|
if allow_mismatches:
|
|
_log.error('%s', exc)
|
|
else:
|
|
raise
|
|
return succeeded
|
|
|
|
|
|
def _apply_one(target: Path, p: VenvPatch) -> None:
|
|
if not target.exists():
|
|
raise CleanError(
|
|
f'venv-patch for {p.path!r}: target file does not exist.'
|
|
' Either remove this patch (if no longer needed) or'
|
|
' update path to match the current upstream layout.'
|
|
)
|
|
text = target.read_text(encoding='utf-8')
|
|
src_count = text.count(p.srctxt)
|
|
repl_count = text.count(p.replacetxt)
|
|
|
|
# Already-applied detection: replacetxt appears the expected
|
|
# number of times AND srctxt is gone. Idempotent re-runs are a
|
|
# no-op in this case.
|
|
if src_count == 0 and repl_count >= p.count:
|
|
_log.debug('venv-patch already applied in %s; skipping.', p.path)
|
|
return
|
|
|
|
if src_count != p.count:
|
|
raise CleanError(
|
|
f'venv-patch for {p.path!r}: expected {p.count}'
|
|
f' occurrence(s) of srctxt, found {src_count}.'
|
|
' Upstream may have changed; update or remove the patch.'
|
|
)
|
|
|
|
new_text = text.replace(p.srctxt, p.replacetxt)
|
|
target.write_text(new_text, encoding='utf-8')
|
|
_log.info('Applied venv-patch in %s.', p.path)
|
|
|
|
|
|
def find_patch_mismatches(
|
|
patches: list[VenvPatch],
|
|
*,
|
|
site_packages: Path | None = None,
|
|
) -> list[str]:
|
|
"""Return the paths of patches not fully applied.
|
|
|
|
Empty list means everything is in the desired state. No
|
|
logging — callers decide severity and message format.
|
|
"""
|
|
if site_packages is None:
|
|
site_packages = _site_packages()
|
|
out: list[str] = []
|
|
for p in patches:
|
|
target = site_packages / p.path
|
|
if not target.exists():
|
|
out.append(p.path)
|
|
continue
|
|
text = target.read_text(encoding='utf-8')
|
|
if text.count(p.srctxt) != 0 or text.count(p.replacetxt) < p.count:
|
|
out.append(p.path)
|
|
return out
|
|
|
|
|
|
def check_patches(
|
|
patches: list[VenvPatch],
|
|
*,
|
|
site_packages: Path | None = None,
|
|
) -> int:
|
|
"""Verify that all patches are present in the current venv.
|
|
|
|
Logs CRITICAL for each mismatched patch. Returns the count;
|
|
callers that want to abort on any mismatch can check the return
|
|
value. Use :func:`find_patch_mismatches` directly if you need
|
|
to control logging severity or message format.
|
|
"""
|
|
paths = find_patch_mismatches(patches, site_packages=site_packages)
|
|
for path in paths:
|
|
_log.critical(
|
|
'venv-patch not applied in %s. The patch may have failed'
|
|
' to apply during venv setup, or upstream may have'
|
|
' changed underneath us.',
|
|
path,
|
|
)
|
|
return len(paths)
|