🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
#!/usr/bin/env python3
# =============================================================================
# fix-recipes-cd.py – analiza i bezpieczna normalizacja komend `cd` w recepturach
# =============================================================================
# PAGBUILD.yaml (fazy build/package). Nie USUWA `cd` (build.sh wchodzi tylko do
# kontenera src-*, a receptura musi sama wejść do katalogu źródeł – usunięcie
# `cd ${pkgname}` zepsułoby build). Zamiast tego:
#
# --check (domyślnie) – klasyfikuje każdy `cd`:
# OK kanoniczne (${pkgname}, ${pkgname}-${pkgver}, generyczne)
# HARDCODED wersja wpisana na sztywno == aktualny pkgver (bezpieczny fix)
# STALE wersja na sztywno != pkgver (po bumpie katalog nie istnieje!)
# SUSPICIOUS nie da się dopasować do żadnego wzorca
# --fix – przepisuje HARDCODED/STALE na ${pkgname}-${pkgver}
# (zachowuje cudzysłowy; --dry-run domyślnie pokazuje diff)
# --no-git – NIE commituj/pushuj zmian (domyślnie: po zapisie zmian
# robi commit + pull --rebase + push do origin, bo inaczej
# pagsync przy następnym syncu odłoży je na stash, a przy
# konflikcie CICHO WYMAŻE przez `git checkout -- .`)
#
# Użycie:
# fix-recipes-cd.py [katalog_receptur] [--fix] [--no-dry-run] [--no-git]
# Domyślny katalog: $PAGAN_RECIPES lub /var/lib/pagan-sync/recipes
# =============================================================================
import os
import re
import subprocess
import sys
import yaml
_GENERIC = {"build", "builddir", "bld", "src", "source", "sources", "work",
"dist", "tmp", "out", "debug", "release", ".", ".."}
def phase_text(phase):
"""Faza build/package z YAML (string lub lista stringów) -> tekst."""
if isinstance(phase, list):
return "\n".join(str(x) for x in phase)
return str(phase or "")
def expand(w, pkgname, pkgver, pkgrel):
w = w.replace("${pkgname}", pkgname).replace("$pkgname", pkgname)
w = w.replace("${pkgver}", pkgver).replace("$pkgver", pkgver)
w = w.replace("${pkgrel}", pkgrel).replace("$pkgrel", pkgrel)
return w
def classify(raw, wanted, pkgname, pkgver):
"""Zwraca (status, sugestia). status ∈ OK/HARDCODED/STALE/SUSPICIOUS.
raw – token przed rozwinięciem (czy zawiera $), wanted – po rozwinięciu.
"""
if (not wanted or wanted in _GENERIC or "*" in wanted
or "$" in wanted or "/" in wanted or wanted in (".", "..")):
return "OK", ""
if wanted == pkgname:
return "OK", ""
if wanted == f"{pkgname}-{pkgver}":
# To samo co ${pkgname}-${pkgver}, ale na sztywno – po bumpie wersji
# katalog nie będzie istniał, dlatego szablon jest bezpieczniejszy.
if "$" in raw:
return "OK", ""
return "HARDCODED", "${pkgname}-${pkgver}"
if wanted.startswith(pkgname + "-"):
# Zastała wersja (nie == pkgver) – katalog nie istnieje po bumpie.
return "STALE", "${pkgname}-${pkgver}"
return "SUSPICIOUS", ""
def scan_recipe(path):
"""Zwraca listę (faza, linia_nr, raw_token, status, sugestia)."""
with open(path, encoding="utf-8") as f:
data = yaml.safe_load(f) or {}
pkgname = str(data.get("pkgname", ""))
pkgver = str(data.get("pkgver", ""))
pkgrel = str(data.get("pkgrel", 1))
out = []
for phase_key in ("build", "package"):
text = phase_text(data.get(phase_key))
for ln, line in enumerate(text.splitlines(), 1):
m = re.match(r'^\s*cd\s+["\']?([^"\'\s;]+)', line)
if not m:
continue
raw = m.group(1)
status, suggestion = classify(raw, expand(raw, pkgname, pkgver, pkgrel),
pkgname, pkgver)
if status != "OK":
out.append((phase_key, ln, raw, status, suggestion))
return pkgname, pkgver, out
def git_commit_and_push(recipes_dir, files_changed):
"""Commit + push zmian receptur do origin (wzorzec jak auto_fix_sha256sums
w pagsync). Bez tego zmiany siedziałyby lokalnie, a następny pagsync
odłożyłby je na stash i mógł CICHO WYMAZAĆ przy konflikcie (checkout -- .)."""
if not os.path.isdir(os.path.join(recipes_dir, ".git")):
print(" ⚠ katalog nie jest repo git – zmiany zostały TYLKO lokalnie")
return False
run = lambda cmd, **kw: subprocess.run(cmd, capture_output=True, text=True,
errors="replace", timeout=60, **kw)
run(["git", "-C", recipes_dir, "add", "-A"])
commit_r = run(["git", "-C", recipes_dir, "commit", "-m",
f"fix: normalizacja cd w recepturach ({files_changed})"])
if commit_r.returncode != 0:
print(f" ⚠ commit nieudany: {commit_r.stderr.strip()[:200]}")
return False
# Rebase lokalnych commitów na origin – push nie zostanie odrzucony
# jako non-fast-forward (jak w pagsync auto_fix_sha256sums).
rebase_r = run(["git", "-C", recipes_dir, "pull", "--rebase", "origin", "main"])
if rebase_r.returncode != 0:
subprocess.run(["git", "-C", recipes_dir, "rebase", "--abort"],
capture_output=True, timeout=10)
print(" ⚠ Rebase przed pushem nieudany – zmiany zostają lokalnie")
print(f" {rebase_r.stderr.strip()[:200]}")
return False
push_r = run(["git", "-C", recipes_dir, "push", "origin", "main"])
if push_r.returncode != 0:
print(f" ⚠ Push do origin NIEUDANY: {push_r.stderr.strip()[:200]}")
print(" Commit został lokalnie – następny pull zrobi rebase i wypchnie.")
return False
print(f" 📤 Wypchnięto do origin ({recipes_dir})")
return True
def main():
args = [a for a in sys.argv[1:] if not a.startswith("--")]
do_fix = "--fix" in sys.argv
dry_run = "--no-dry-run" not in sys.argv
no_git = "--no-git" in sys.argv
recipes_dir = (args[0] if args else
os.environ.get("PAGAN_RECIPES", "/var/lib/pagan-sync/recipes"))
if not os.path.isdir(recipes_dir):
print(f"❌ Brak katalogu receptur: {recipes_dir}")
print(" Podaj ścieżkę jako argument albo ustaw PAGAN_RECIPES.")
sys.exit(2)
found = fixable = stale = suspicious = 0
written = 0
for root, _dirs, files in os.walk(recipes_dir):
if "PAGBUILD.yaml" not in files:
continue
path = os.path.join(root, "PAGBUILD.yaml")
try:
pkgname, pkgver, issues = scan_recipe(path)
except Exception as e:
print(f"⚠ {path}: błąd parsowania YAML – {e}")
continue
if not issues:
continue
found += 1
print(f"\n📄 {path} ({pkgname}-{pkgver})")
fix_items = []
for phase, ln, raw, status, suggestion in issues:
if status == "HARDCODED":
fixable += 1
elif status == "STALE":
stale += 1
else:
suspicious += 1
print(f" [{phase}:{ln}] cd {raw:.<40} {status}"
+ (f" → {suggestion}" if suggestion else ""))
if do_fix and status in ("HARDCODED", "STALE"):
fix_items.append((raw, suggestion))
if do_fix and fix_items:
# Celowana podmiana tekstu (bez przebudowy YAML – safe_dump m.in.
# zamieniał nowe linie na literalne \\n i psuł recepturę).
with open(path, encoding="utf-8") as f:
content = f.read()
for raw, suggestion in fix_items:
# (?:\)? – obsługa escapowanych cudzysłowów w YAML-owych
# stringach (faza jako lista: - "cd \"baz-3.1\"").
pattern = re.compile(r'(\bcd\s+(?:\\)?["\']?)' + re.escape(raw)
+ r'(?=(?:\\)?["\'\s;]|$)')
content, n = pattern.subn(lambda m: m.group(1) + suggestion, content)
if n:
print(f" ✏️ cd {raw} → cd {suggestion} ({n}×)")
else:
print(f" ⚠ nie znaleziono w pliku: cd {raw} (pominięto)")
if dry_run:
print(" (dry-run – nie zapisano; użyj --no-dry-run, by zapisać)")
else:
with open(path, "w", encoding="utf-8") as f:
f.write(content)
written += 1
print(" ✅ zapisano")
print(f"\n📊 Podsumowanie: {found} receptur z problematycznym cd | "
f"HARDCODED: {fixable} | STALE: {stale} | SUSPICIOUS: {suspicious}")
if not do_fix:
print("Uruchom z --fix (--no-dry-run, by zapisać), aby przepisać HARDCODED/STALE.")
return
if written and not dry_run and not no_git:
git_commit_and_push(recipes_dir, written)
elif written and dry_run:
print("(dry-run – nie zapisano nic, brak commita)")
elif written and no_git:
print(" ⚠ --no-git: zmiany zostały lokalnie, NIE wypchnięte do origin!")
if __name__ == "__main__":
main()