mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-10 21:43:23 -05:00
generate_pack.py held six responsibilities in 4744 lines. Five move out in dependency order, so nothing above reaches back down: destinations, core extras, resolution with its storage tiers, the notes shipped inside a pack, and the pack verifier. generate_pack.py keeps the build and the command line, and re-exports the rest. Two things the move surfaced. The offline switch was a module global the command line assigned, which a re-export would have copied and frozen at False; it is set through a call now and not re-exported. And a facade placed after the entry point binds too late: importing the module worked, running it did not, so the manifest run died on a name the tests never exercised because tests import. Verified against the previous revision on frozen inputs: every generated artefact is identical, save the catalogue that embeds hashes of files carrying a build timestamp.
103 lines
3.6 KiB
Python
103 lines
3.6 KiB
Python
"""Writing generated files, and not writing them.
|
|
|
|
A generated artefact carries a timestamp but must not be rewritten
|
|
when only the clock moved: the CI freshness guard is a git diff, and
|
|
it is only meaningful if the comparison ignores the hour."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import os
|
|
import re
|
|
|
|
|
|
_TIMESTAMP_PATTERNS = [
|
|
re.compile(r'"generated_at":\s*"[^"]*"'), # database.json
|
|
re.compile(r'"imported_at":\s*"[^"]*"'), # provenance snapshots
|
|
re.compile(r"\*Auto-generated on [^*]*\*"), # README.md
|
|
re.compile(r"\*Generated on [^*]*\*"), # docs site pages
|
|
# The decorated pages carry the same stamp again as a rendered element,
|
|
# and missing it rewrote every page on every run for the clock alone.
|
|
re.compile(r'<div class="rb-timestamp">[^<]*</div>'),
|
|
]
|
|
|
|
def write_if_changed(path: str, content: str, normalize=None) -> bool:
|
|
"""Write content to path only if the non-timestamp content differs.
|
|
|
|
Compares new and existing content after stripping timestamp lines.
|
|
Returns True if the file was written, False if skipped (unchanged).
|
|
|
|
A caller that writes a file in two passes -a body, then the same body
|
|
wrapped in front matter -passes ``normalize`` to reduce both sides to the
|
|
part it owns. Without it the second pass always sees a difference, the
|
|
file is rewritten, and the fresh timestamp defeats the comparison.
|
|
"""
|
|
if os.path.exists(path):
|
|
with open(path) as f:
|
|
existing = f.read()
|
|
before, after = (
|
|
(normalize(existing), normalize(content))
|
|
if normalize
|
|
else (existing, content)
|
|
)
|
|
if _strip_timestamps(before) == _strip_timestamps(after):
|
|
return False
|
|
with open(path, "w") as f:
|
|
f.write(content)
|
|
return True
|
|
|
|
def _strip_timestamps(text: str) -> str:
|
|
"""Remove known timestamp patterns for content comparison."""
|
|
result = text
|
|
for pattern in _TIMESTAMP_PATTERNS:
|
|
result = pattern.sub("", result)
|
|
return result
|
|
|
|
class ArtifactLockBusy(RuntimeError):
|
|
"""Raised when another process already holds the artifact directory."""
|
|
|
|
@contextlib.contextmanager
|
|
def artifact_lock(directory: str, exclusive: bool = True):
|
|
"""Serialize access to a shared artifact directory across processes.
|
|
|
|
Two pipeline runs building the same dist/ leave readers looking at
|
|
half-written ZIPs, which surfaces as BadZipFile far from its cause.
|
|
Writers take the lock exclusively, readers share it. On platforms
|
|
without flock the lock is a no-op.
|
|
"""
|
|
try:
|
|
import fcntl
|
|
except ImportError:
|
|
yield
|
|
return
|
|
|
|
os.makedirs(directory, exist_ok=True)
|
|
lock_path = os.path.join(directory, ".lock")
|
|
mode = fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH
|
|
with open(lock_path, "w") as handle:
|
|
try:
|
|
fcntl.flock(handle, mode | fcntl.LOCK_NB)
|
|
except OSError as exc:
|
|
raise ArtifactLockBusy(
|
|
f"{directory} is in use by another run "
|
|
f"(lock: {lock_path}). Wait for it to finish."
|
|
) from exc
|
|
try:
|
|
yield
|
|
finally:
|
|
fcntl.flock(handle, fcntl.LOCK_UN)
|
|
|
|
def _build_timestamp(db: dict | None = None) -> str:
|
|
"""Timestamp for generated artifacts.
|
|
|
|
Reads the database snapshot the artifact was built from, so rebuilding
|
|
the same data twice yields the same value. Falls back to the clock only
|
|
when no database is at hand.
|
|
"""
|
|
stamp = (db or {}).get("generated_at")
|
|
if isinstance(stamp, str) and stamp:
|
|
return stamp
|
|
from datetime import datetime, timezone
|
|
|
|
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|