#!/usr/bin/env python3 """ ================================================================================ migrate-recipes.py — konwertuje receptury cards/NuTyX (Pkgfile) na PAGBUILD.yaml ================================================================================ Przenosi system receptur z formatu `cards` (bash Pkgfile) do natywnego formatu PaganOS (PAGBUILD.yaml) używanego przez `pagbuild`, zachowując lokalne pliki (patche, service, config). Footprinty (pliki `*.footprint`) są CELOWO pomijane: PaganOS działa jak Arch/Debian — pakiet powstaje z katalogu DESTDIR, a `pag` sam śledzi listę plików i sumy SHA256 (/var/lib/pag/files.db). Zadeklarowane w footprintach podpakiety (devel/doc/man/językowe) są scalane z powrotem do pakietu głównego, więc ich lista plików jest zbędna. Mapowanie pól cards -> PAGBUILD.yaml: description -> pkgdesc url -> url license -> license (lista) name -> pkgname version -> pkgver release -> pkgrel makedepends + PKGMK_DEPENDS-> makedepends depends + run -> depends source -> source (URL-e rozwiązane, lokalne pliki kopiowane) prepare()/prepre()/unpack_source() -> prefiks fazy build build() | build= -> build (szablony 1:1 z pkgmk NuTyX) package() | packager() -> package *.footprint -> (pomijane — patrz nota o footprintach wyżej) *.post-install itd. + echo-generowane -> hooks: (post-install/pre-install/...) Domyślny build (bez build()/build=) jak w cards: autotools, a dla źródeł .deb – rozpakowanie payloadu. Subpakiety PKGMK_GROUPS są scalane do jednego pakietu (format pagbuild) – funkcje grup przenoszące pliki są pomijane, a te wykonujące realną pracę (unpack_source/prepre/packager) zostają wkomponowane w fazy build/package. Pomijane (na życzenie): packager, contributors, maintainer. Użycie: python3 migrate-recipes.py [--source DIR] [--dest DIR] [--dry-run] [--delete-source] [--brand] [--only NAME] [--limit N] [--verbose] ================================================================================ """ import argparse import os import re import shlex import shutil import sys try: import yaml except ImportError: print("❌ Brak PyYAML. Zainstaluj: pip install pyyaml (lub python3-yaml)", file=sys.stderr) sys.exit(2) # ── Mapowanie kategorii źródłowych (x86_64-testingd) na docelowe (recipes/) ── CATEGORY_MAP = { "base": "core", "cli": "utils", "cli-extra": "utils", "gui": "gui", "gui-extra": "gui", } # ── Szablony faz dla predefiniowanych typów build= (1:1 z pkgmk NuTyX) ── # {src_dir} – katalog źródłowy (np. attr-2.6.0 / libICE-1.1.2) podstawiany # w trakcie migracji; {mopts} – PKGMK_MESON_OPTIONS; {pkg} – nazwa # pakietu pythona (np. pysmbc). BUILD_TEMPLATES = { "meson": { "build": "meson setup --prefix /usr --buildtype plain --libexecdir lib --sbindir bin -D b_pie=true {src_dir} builddir{mopts}\nmeson compile -C builddir", "package": 'DESTDIR="${{PKGDIR}}" meson install -C builddir', "needs_review": False, }, "autotools": { "build": "cd {src_dir}\n./configure --prefix=/usr --disable-static\nmake", "package": 'make DESTDIR="${{PKGDIR}}" install', "needs_review": False, }, "cmake": { "build": "cmake -B build -S {src_dir} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_LIBDIR=lib -DBUILD_TESTING=OFF -Wno-dev\ncmake --build build", "package": 'DESTDIR="${{PKGDIR}}" cmake --install build', "needs_review": False, }, "kde5": { "build": "cmake -B build -S {src_dir} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_LIBDIR=lib -DBUILD_TESTING=OFF -Wno-dev\ncmake --build build", "package": 'DESTDIR="${{PKGDIR}}" cmake --install build', "needs_review": False, }, "kde6": { "build": "cmake -B build -S {src_dir} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_LIBDIR=lib -DBUILD_TESTING=OFF -Wno-dev\ncmake --build build", "package": 'DESTDIR="${{PKGDIR}}" cmake --install build', "needs_review": False, }, "xorg": { "build": "cd {src_dir}\n./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var --disable-static\nmake", "package": 'make DESTDIR="${{PKGDIR}}" install', "needs_review": False, }, "python3": { "build": 'cd {src_dir}\npip3 wheel -w dist --no-build-isolation --no-deps .\npip3 install --no-index --find-links dist --no-cache-dir --no-user --root="${{PKGDIR}}" {pkg}', "package": "true", "needs_review": False, }, } # Domyślny build cards (brak build()/build=): autotools; dla źródeł .deb – # rozpakowanie payloadu (cards robi to samo po rozpakowaniu archiwum .deb). DEFAULT_PACKAGE_BODY = 'make DESTDIR="${PKGDIR}" install' DEFAULT_BUILD_AUTOTOOLS = "./configure --prefix=/usr\nmake" DEB_PACKAGE_BODY = 'bsdtar -xf data.tar.* -C "${PKGDIR}"' # Funkcje grup (subpakiety) bezpieczne do pominięcia przy scalaniu w 1 pakiet: # tylko przenoszą pliki z pakietu głównego do subpakietu, więc po scaleniu # pliki po prostu zostają w pakiecie głównym. (packager() jest dołączany do # fazy package – wykonuje realną pracę, np. instalację licencji.) SAFE_DROP_GROUP_FNS = { "devel", "man", "doc", "lib", "service", "pack_locale", "pack_remove_locale", } # Podmiany zmiennych w generowanych skryptach hooków (post-install). HOOK_VAR_FALLBACK = {"BRANCH": "systemd"} # ============================================================================= # Parsowanie Pkgfile # ============================================================================= def _unquote(s: str) -> str: s = s.strip() if len(s) >= 2 and s[0] in ('"', "'") and s[-1] == s[0]: return s[1:-1] return s def _split_items(s: str) -> list: s = s.strip() if not s: return [] s = re.sub(r"\\[ \t]*\n[ \t]*", " ", s) s = re.sub(r"\\$", "", s) try: parts = shlex.split(s, posix=True) except ValueError: parts = s.split() return [p for p in parts if p != ""] def parse_pkgfile(text: str): """Zwraca (scalars, arrays, functions).""" lines = text.splitlines() scalars = {} arrays = {} functions = {} i, n = 0, len(lines) while i < n: stripped = lines[i].strip() if not stripped or stripped.startswith("#"): i += 1 continue mfunc = re.match(r"^([A-Za-z_][A-Za-z0-9_]*)\s*\(\s*\)\s*\{?\s*$", stripped) if mfunc: fname = mfunc.group(1) has_brace = stripped.endswith("{") i += 1 if not has_brace: if i < n and lines[i].strip() == "{": i += 1 body = [] while i < n and lines[i].strip() != "}": body.append(lines[i]) i += 1 if i < n: i += 1 functions[fname] = "\n".join(body) continue marr = re.match(r"^([A-Za-z_][A-Za-z0-9_]*)=\(\s*(.*)$", stripped) if marr: vname = marr.group(1) rest = marr.group(2) if rest.rstrip().endswith(")"): content = rest.rstrip()[:-1] i += 1 else: buf = [rest] i += 1 while i < n: line = lines[i] buf.append(line) i += 1 if line.rstrip().endswith(")"): break joined = "\n".join(buf) content = joined.rstrip()[:-1] arrays[vname] = _split_items(content) continue mscalar = re.match(r"^([A-Za-z_][A-Za-z0-9_]*)=(.*)$", stripped) if mscalar: vname = mscalar.group(1) value = mscalar.group(2).strip() while value.endswith("\\") and i + 1 < n: i += 1 value = value[:-1].rstrip() + " " + lines[i].strip() scalars[vname] = _unquote(value) i += 1 continue i += 1 return scalars, arrays, functions # ============================================================================= # Translacja ciała build/package (cards -> pagbuild) # ============================================================================= def _dedent(s: str) -> str: if not s: return s lines = s.split("\n") ref = None for l in lines: if l.strip() != "": ref = len(l) - len(l.lstrip(" \t")) break if ref is None or ref == 0: return s.strip("\n") out = [] for l in lines: lead = len(l) - len(l.lstrip(" \t")) out.append(l[min(ref, lead):]) return "\n".join(out).strip("\n") def translate_body(body: str, scalars: dict = None, pkgmk_root: str = None, arch: str = "x86_64") -> str: """Tłumaczy ciało fazy z dialektu cards na pagbuild.""" if not body: return body # Katalog roboczy pagbuild = katalog źródeł (build.sh robi tam cd). body = re.sub(r"\$\{SRC\}", ".", body) body = re.sub(r"\$SRC\b", ".", body) body = re.sub(r"\$\{PKGMK_SOURCE_DIR\}", ".", body) body = re.sub(r"\$PKGMK_SOURCE_DIR\b", ".", body) if pkgmk_root: body = re.sub(r"\$\{PKGMK_ROOT\}", pkgmk_root, body) body = re.sub(r"\$PKGMK_ROOT\b", pkgmk_root, body) body = re.sub(r"\$\{PKGMK_ARCH\}", arch, body) body = re.sub(r"\$PKGMK_ARCH\b", arch, body) body = re.sub(r"\$\{PKG\}", "${PKGDIR}", body) body = re.sub(r"\$PKG\b", "${PKGDIR}", body) body = re.sub(r"\$\{name\}", "${pkgname}", body) body = re.sub(r"\$name\b", "${pkgname}", body) body = re.sub(r"\$\{version\}", "${pkgver}", body) body = re.sub(r"\$version\b", "${pkgver}", body) body = re.sub(r"\$\{release\}", "${pkgrel}", body) body = re.sub(r"\$release\b", "${pkgrel}", body) # Skalarne zmienne _* z Pkgfile (np. _name=libICE) nie istnieją w środowisku # pagbuild – wstawiamy ich wartości wprost. Zmienne lokalne funkcji build() # (nie-skalarne) zostają nietknięte. if scalars: pkgname_v = str(scalars.get("name", "")) pkgver_v = str(scalars.get("version", "")) # Klucze metadanych receptury – nie są zmiennymi środowiska budowy. meta = {"name", "version", "release", "description", "url", "license", "maintainer", "packager", "contributors", "build", "arch", "ptodate", "uptodate", "renames", "alias", "setname"} for var, val in scalars.items(): var_s = str(var) if var_s in meta or var_s.startswith("PKGMK_") or not str(val).strip(): continue val_bare = str(val).strip().strip('"').strip("'") if val_bare in ("$name", "${name}"): val = pkgname_v elif val_bare in ("$version", "${version}"): val = pkgver_v expanded = _expand_scalar(val, scalars, pkgname_v, pkgver_v) # Zmienne _* wstawiaj zawsze (historyczne zachowanie). Pozostałe # (np. buildr=174877, KERNELNAME=kernel-612) tylko gdy wartość jest # prostym literałem – nie wstrzykujemy złożonych konstrukcji bash. if not var_s.startswith("_") and not re.fullmatch( r"[A-Za-z0-9_./+\-:]+", expanded.strip()): continue body = re.sub(r"\$\{" + re.escape(var_s) + r"\}", expanded, body) body = re.sub(r"\$" + re.escape(var_s) + r"\b", expanded, body) # Rozwiń bashowe formy parametrów (${version:0:4}, ${name#...}, ${_name}...) # w ciele fazy – pagbuild eksportuje tylko pkgname/pkgver/pkgrel, więc # ${version:...} i ${name...} muszą zostać wyliczone już na etapie migracji. if scalars: body = _expand_scalar(body, scalars, str(scalars.get("name", "")), str(scalars.get("version", ""))) return body SOURCE_VAR_MAP = { "name": "${pkgname}", "version": "${pkgver}", "release": "${pkgrel}", } def resolve_source_items(items: list, scalars: dict) -> list: version = str(scalars.get("version", "")) def repl(m): var = m.group(1) if var in SOURCE_VAR_MAP: return SOURCE_VAR_MAP[var] if var in scalars and str(scalars[var]).strip() != "": # Rozwiń wartość skalara (np. _version=${version%.*}). return _expand_scalar(str(scalars[var]), scalars, str(scalars.get("name", "")), version) return m.group(0) out = [] for it in items: # Najpierw rozwinięcia parametrów bash w URL (${version%.*}, ${version:0:4}, ...). s = _expand_scalar(it, scalars, str(scalars.get("name", "")), version) # Podmiana zmiennych do punktu stałego (obsługuje zagnieżdżenia). for _ in range(4): new = re.sub(r"\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?", repl, s) if new == s: break s = new if is_url(s): s = _normalize_url(s) if version: s = re.sub( r"(? bool: return bool(re.match(r"^[a-zA-Z][a-zA-Z0-9+.-]*://", s)) def _normalize_url(s: str) -> str: """Zwija wielokrotne / w ścieżce URL (np. individual//driver -> individual/driver), nie naruszając schematu http:// ani query/fragmentu.""" m = re.match(r"^([a-zA-Z][a-zA-Z0-9+.-]*://)([^?#]*)(.*)$", s) if not m: return s return m.group(1) + re.sub(r"/{2,}", "/", m.group(2)) + m.group(3) # ============================================================================= # Budowanie dokumentu YAML # ============================================================================= def _glob_to_regex(pat: str) -> str: out = "" i = 0 while i < len(pat): c = pat[i] if c == "*": out += ".*" elif c == "?": out += "." elif c == "[": j = pat.find("]", i + 1) if j > i: out += pat[i:j + 1] i = j else: out += r"\[" else: out += re.escape(c) i += 1 return out def _expand_scalar(value, scalars, pkgname, pkgver): """Rozwija ${var%sufiks} / ${var#prefiks} (parametry bash) w wartościach skalarnych, np. _version=${version%.*} -> 7.1.""" value = str(value) def repl(m): expr = m.group(1) # ${var:off:len} – podciąg (np. ${version:0:4}) msub = re.match(r"^([A-Za-z_]\w*):(-?\d*)(?::(-?\d*))?$", expr) if msub: var, off, ln = msub.group(1), msub.group(2) or "", msub.group(3) or "" if var == "version": val = pkgver elif var == "name": val = pkgname elif var in scalars: val = str(scalars[var]) else: return m.group(0) o = int(off or 0) return val[o:] if ln == "" else val[o:o + int(ln)] mm = re.match(r"^([A-Za-z_]\w*)(//|%%|%|##|#|/)(.*)$", expr) if not mm: return m.group(0) var, op, rest = mm.group(1), mm.group(2), mm.group(3) if var == "version": val = pkgver elif var == "name": val = pkgname elif var in scalars: val = str(scalars[var]) else: return m.group(0) if op in ("/", "//"): # ${var/wzorzec/zamiana} – podstawienie zamiast przycięcia if "/" in rest: pat, repl_text = rest.split("/", 1) else: pat, repl_text = rest, "" rx = _glob_to_regex(pat) count = 0 if op == "//" else 1 return re.sub(rx, repl_text, val, count=count) rx = _glob_to_regex(rest) if op.startswith("%"): order = range(len(val) - 1, -1, -1) if op == "%" else range(0, len(val)) for k in order: if re.fullmatch(rx, val[k:]): return val[:k] return val order = range(1, len(val) + 1) if op == "#" else range(len(val), 0, -1) for k in order: if re.fullmatch(rx, val[:k]): return val[k:] return val for _ in range(3): new = re.sub(r"\$\{([^{}]+)\}", repl, value) if new == value: break value = new return value def _subst_hook_vars(text: str, scalars: dict, pkgname: str, pkgver: str) -> str: """Podstawia znane wartości w treści skryptu hooka (post-install).""" def repl(m): var = m.group(1) if var == "name": return pkgname if var == "version": return pkgver if var == "release": return str(scalars.get("release", "1")) if var == "KERNELRELEASE": if pkgname.startswith("kernel"): # Wartość znana dopiero po instalacji – moduły są w # /usr/lib/modules/ (obok katalogu $name). return f"$(ls /usr/lib/modules | grep -vx '{pkgname}' | head -1)" raw = scalars.get("_version") or scalars.get("version") or m.group(0) return _expand_scalar(raw, scalars, pkgname, pkgver) if var in scalars and str(scalars[var]).strip(): return _expand_scalar(scalars[var], scalars, pkgname, pkgver) if var in HOOK_VAR_FALLBACK: return HOOK_VAR_FALLBACK[var] return m.group(0) return re.sub(r"\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?", repl, text) _HOOK_ECHO_RE = re.compile( r'echo\s+"(.*?)"\s*>\s*\$\{?PKGMK_ROOT\}?/\$\{?name\}?\s*\.' r"(post-install|pre-install|post-remove|pre-remove)", re.DOTALL, ) def extract_generated_hooks(body: str, scalars: dict, pkgname: str, pkgver: str): """Wyciąga `echo "..." > $PKGMK_ROOT/$name.post-install` z fazy build i zwraca (nowe_ciało, {nazwa_hooka: treść}).""" hooks = {} def repl(m): content = m.group(1) hook = m.group(2) content = content.replace('\\"', '"').replace("\\\\", "\\") content = _subst_hook_vars(content, scalars, pkgname, pkgver) hooks.setdefault(hook, []).append(content.strip()) return "" new_body = _HOOK_ECHO_RE.sub(repl, body) return new_body, {k: "\n".join(v) for k, v in hooks.items()} def _normalize_deps(items, known_names, own_name): """Po scaleniu subpakietów (pkg.lib/pkg.devel) normalizuje zależności: - własny subpakiet (vlc -> vlc.lib) – pomiń (pliki są w tym samym pakiecie), - subpakiet innego pakietu (epiphany -> libxslt.lib) – mapuj na pakiet bazowy, - nieistniejące pakiety (np. cards.devel) – pomiń.""" out = [] for it in items: it = str(it).strip() if not it: continue if "." in it: base = it.split(".", 1)[0] if base == own_name: continue if it in (known_names or set()): out.append(it) continue if base in (known_names or set()): out.append(base) continue out.append(it) return _unique(out) def build_yaml(pkgdir: str, scalars: dict, arrays: dict, functions: dict, known_names: set = None): review = [] hook_files = [] pkgname = scalars.get("name", "") or os.path.basename(pkgdir) pkgver = scalars.get("version", "") if not pkgname or not pkgver: raise ValueError("brak name/version w Pkgfile") pkgrel = scalars.get("release", "1") pkgdesc = scalars.get("description", "") url = scalars.get("url", "") arch = scalars.get("arch", scalars.get("PKGMK_ARCH", "x86_64")) license_raw = scalars.get("license", "") if isinstance(license_raw, str) and license_raw.strip(): licenses = [l.strip() for l in re.split(r"[,;]", license_raw) if l.strip()] else: licenses = [] makedepends = list(arrays.get("makedepends", [])) pk_depends = list(arrays.get("PKGMK_DEPENDS", [])) makedepends = _unique(makedepends + pk_depends) depends = list(arrays.get("depends", [])) run = list(arrays.get("run", [])) depends = _unique(depends + run) # Po scaleniu subpakietów: vlc.lib -> (pomiń), libxslt.lib -> libxslt. depends = _normalize_deps(depends, known_names, pkgname) makedepends = _normalize_deps(makedepends, known_names, pkgname) source = resolve_source_items(arrays.get("source", []), scalars) resolved = list(source) # Nazwa katalogu źródłowego po rozpakowaniu oraz odpowiednik $PKGMK_ROOT. _name = scalars.get("_name", "") or pkgname if str(_name).strip().strip('"').strip("'") in ("$name", "${name}"): _name = pkgname # idiom cards: _name=$name if pkgname == "tcl": src_dir = pkgname + pkgver # tcl rozpakowuje się do tcl (bez myślnika) else: src_dir = f"{_name}-{pkgver}" pkgmk_root = f"/tmp/src-{pkgname}-{pkgver}/{src_dir}" def tr(body): return translate_body(body, scalars=scalars, pkgmk_root=pkgmk_root, arch=arch) build_type = scalars.get("build", None) if build_type and build_type not in functions: if build_type not in BUILD_TEMPLATES: review.append(f"nieznany build={build_type!r}") build_type = None # ── Hooki z lokalnych plików: {name}[-.service].{pre|post}-{install|remove} ── local_hooks = {} for fn in sorted(os.listdir(pkgdir)): key = None for suffix in ("post-install", "pre-install", "pre-remove", "post-remove"): if fn in (f"{pkgname}.{suffix}", f"{pkgname}.service.{suffix}"): key = suffix break if key is None: continue with open(os.path.join(pkgdir, fn), encoding="utf-8", errors="replace") as f: content = f.read().strip() local_hooks[key] = (local_hooks[key] + "\n" + content) if key in local_hooks else content hook_files.append(fn) # ── Funkcje grup ── unpack_src = functions.get("unpack_source") prepre = functions.get("prepre") packager_fn = functions.get("packager") reset_cd = f"cd /tmp/src-{pkgname}-{pkgver}" pre_build = [] if unpack_src is not None: # Jeżeli funkcja używa helperów pkgmk (get_filename/$source), domyślne # rozpakowywanie pagbuild robi dokładnie to samo – można pominąć. if "get_filename" in unpack_src or "${source" in unpack_src or " $source" in unpack_src: pass else: pre_build.append(_dedent(tr(unpack_src))) pre_build.append(reset_cd) if prepre is not None: pre_build.append(_dedent(tr(prepre))) pre_build.append(reset_cd) prepare = functions.get("prepare") if prepare is not None: pre_build.append(_dedent(tr(prepare))) pre_build.append(reset_cd) deb_source = next((s for s in resolved if re.search(r"\.deb$", s)), None) # Pakiety ze źródłem .deb i własnym build() zakładają, że cards rozpakował # już archiwum .deb do data.tar.* w katalogu roboczym – odtwórz to. if deb_source and functions.get("build") is not None: pre_build.insert(0, reset_cd) pre_build.insert(0, "bsdtar -xf *.deb") build_fn = functions.get("build") hooks = {} if build_fn is not None: # Hooki generowane w build() wyciągamy z SUROWEGO ciała (przed # tłumaczeniem $PKGMK_ROOT/$name na ścieżki pagbuild). W cards build() # NADPISUJE plik $PKGMK_ROOT/$name.post-install – generowane hooki # mają więc pierwszeństwo przed lokalnymi. raw_body = _dedent(build_fn) raw_body, gen_hooks = extract_generated_hooks(raw_body, scalars, pkgname, pkgver) hooks.update(gen_hooks) build_body = _dedent(tr(raw_body)) elif build_type: tpl = BUILD_TEMPLATES[build_type] mopts = scalars.get("PKGMK_MESON_OPTIONS", "") mopts_str = (" " + " ".join(mopts.split())) if (build_type == "meson" and mopts) else "" build_body = tpl["build"].format(src_dir=src_dir, mopts=mopts_str, pkg=_name) elif deb_source: # cards: dla źródła .deb domyślny build = payload do $PKG build_body = "bsdtar -xf *.deb" else: # cards: domyślny build = autotools build_body = f"cd {src_dir}\n{DEFAULT_BUILD_AUTOTOOLS}" if pre_build: build_body = "\n".join(pre_build) + "\n" + build_body package_fn = functions.get("package") if package_fn is not None: package_body = _dedent(tr(package_fn)) elif build_fn is not None: # build() sam instaluje do ${PKGDIR}? – faza package pusta, inaczej # domyślne make install (jak w cards). if "${PKGDIR}" in build_body or "DESTDIR" in build_body: package_body = "true" else: package_body = f"cd {src_dir}\n{DEFAULT_PACKAGE_BODY}" elif deb_source: package_body = DEB_PACKAGE_BODY elif build_type: package_body = BUILD_TEMPLATES[build_type]["package"].format( src_dir=src_dir, pkg=_name) else: package_body = f"cd {src_dir}\n{DEFAULT_PACKAGE_BODY}" if packager_fn is not None: # packager() w cards jest wykonywany po build() – dodajemy na początek # fazy package (np. instalacja licencji w gimp). package_body = _dedent(tr(packager_fn)) + "\n" + package_body for b in (build_body, package_body): # Pomijamy komentarze (# post-install autogen itp. – same echo jest # już przeniesione do hooks:). code = "\n".join(l for l in b.split("\n") if not l.strip().startswith("#")) if "$PKGMK_ROOT" in code or "post-install" in code: review.append("generuje/odwołuje się do post-install (wymaga hooków pag)") break # Lokalne hooki (bez wygenerowanego odpowiednika) – prolog z wartościami # z receptury, bo cards udostępniał je skryptom hooków. for k, v in local_hooks.items(): if k in hooks: continue hooks[k] = f"name={pkgname}\nversion={pkgver}\nrelease={pkgrel}\n" + v # Scalanie subpakietów PKGMK_GROUPS jest zamierzone (pagbuild buduje 1 pakiet). merged_subpkgs = bool(arrays.get("PKGMK_GROUPS")) known = {"prepare", "build", "package", "uptodate", "ptodate", "unpack_source", "prepre", "packager"} | SAFE_DROP_GROUP_FNS group_fns = [k for k in functions if k not in known and not k.startswith("locale_")] if group_fns: review.append(f"nieznane funkcje grup porzucone (scalenie): {', '.join(group_fns)}") update_script = None for fname in ("uptodate", "ptodate"): if functions.get(fname, "").strip(): update_script = _dedent(tr(functions[fname])) break doc = { "pkgname": pkgname, "pkgver": pkgver, "pkgrel": pkgrel, "pkgdesc": pkgdesc, "url": url, "arch": arch, } if licenses: doc["license"] = licenses doc["depends"] = depends doc["makedepends"] = makedepends doc["source"] = resolved doc["sha256sums"] = ["SKIP"] * len(resolved) if hooks: doc["hooks"] = hooks doc["build"] = build_body doc["package"] = package_body if update_script: doc["update"] = update_script return doc, review, hook_files, merged_subpkgs def _unique(items): seen = set() out = [] for it in items: it = it.strip() if it and it not in seen: seen.add(it) out.append(it) return out # ============================================================================= # Emisja YAML # ============================================================================= def _q(s) -> str: s = str(s) if s == "": return "''" if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", s): return s return "'" + s.replace("'", "''") + "'" def _block(key: str, value: str) -> str: if value is None: value = "" value = str(value) if value == "": return f"{key}: ''" lines = value.split("\n") nonempty = [l for l in lines if l.strip() != ""] if not nonempty: return f"{key}: ''" min_indent = min(len(l) - len(l.lstrip(" \t")) for l in nonempty) out = [f"{key}: |"] for l in lines: if l.strip() == "": out.append("") else: out.append(" " + l[min_indent:]) return "\n".join(out) + "\n" def _list_block(key: str, items) -> str: if not items: return f"{key}: []" out = [f"{key}:"] for it in items: out.append("- " + _q(it)) return "\n".join(out) + "\n" def _hooks_block(items) -> str: if not items: return "" out = ["hooks:"] for k, v in items.items(): out.append(" " + _q(k) + ": |") for line in str(v).split("\n"): out.append(" " + line) return "\n".join(out) + "\n" def dump_yaml(doc: dict) -> str: parts = [] for key, val in doc.items(): if key == "hooks": parts.append(_hooks_block(val)) elif key in ("build", "package", "update"): parts.append(_block(key, val)) elif isinstance(val, list): parts.append(_list_block(key, val)) else: parts.append(f"{key}: {_q(val)}") return "\n".join(parts) # ============================================================================= # Branding (opcjonalnie) # ============================================================================= BRAND_RE = re.compile(r"NutyX|Nutyx|nutyx", re.IGNORECASE) def apply_brand(doc: dict) -> None: if isinstance(doc.get("pkgdesc"), str): doc["pkgdesc"] = BRAND_RE.sub("PaganLinux", doc["pkgdesc"]) # ============================================================================= # Optymalizacja odczytu plików (zastępuje find_existing w pętli) # ============================================================================= def build_existing_index(recipes_root: str) -> dict: """Buduje indeks {pkgname: dirpath} jednorazowo w pamięci dla O(1) wyszukiwania.""" index = {} if not os.path.exists(recipes_root): return index for dirpath, dirnames, filenames in os.walk(recipes_root): if "PAGBUILD.yaml" not in filenames: continue try: with open(os.path.join(dirpath, "PAGBUILD.yaml"), encoding="utf-8") as f: data = yaml.safe_load(f) or {} pkg = data.get("pkgname") if pkg: index[pkg] = dirpath except Exception: pass return index # ============================================================================= # Składowanie + raport # ============================================================================= def collect_pkgfiles(root: str): found = [] for cat in sorted(os.listdir(root)): catdir = os.path.join(root, cat) if not os.path.isdir(catdir): continue for name in sorted(os.listdir(catdir)): pkgdir = os.path.join(catdir, name) pkgfile = os.path.join(pkgdir, "Pkgfile") if os.path.isfile(pkgfile): found.append((cat, name, pkgdir, pkgfile)) return found def main(): ap = argparse.ArgumentParser(description="Migruj Pkgfile (cards) -> PAGBUILD.yaml") ap.add_argument("--source", default="x86_64-testingd") ap.add_argument("--dest", default="recipes") ap.add_argument("--dry-run", action="store_true") ap.add_argument("--delete-source", action="store_true", help="Usuń źródłowe drzewo x86_64-testingd po migracji") ap.add_argument("--no-overwrite", action="store_true", help="Nie nadpisuj istniejących PAGBUILD.yaml w recipes/ (tylko dodawaj nowe)") ap.add_argument("--brand", action="store_true", help="Zamień NutyX/nutyx -> PaganLinux w pkgdesc") ap.add_argument("--only", help="Migruj tylko podany pakiet (nazwa)") ap.add_argument("--limit", type=int, help="Migruj tylko N pierwszych pakietów") ap.add_argument("--verbose", action="store_true") args = ap.parse_args() source_root = os.path.abspath(args.source) dest_root = os.path.abspath(args.dest) if not os.path.isdir(source_root): print(f"❌ Źródło {source_root} nie istnieje.", file=sys.stderr) sys.exit(2) entries = collect_pkgfiles(source_root) if args.only: entries = [e for e in entries if e[1] == args.only] if args.limit: entries = entries[: args.limit] # Zbiór wszystkich nazw pakietów (do normalizacji zależności po scaleniu). known_names = {name for (_cat, name, _dir, _pf) in entries} total_entries = len(entries) print(f"📦 Znaleziono {total_entries} receptur do migracji.", file=sys.stderr) # Inicjalizacja cache przed wejściem w pętlę if not args.dry_run and not args.only: print("🔍 Zbieranie informacji o już istniejących pakietach (cache)...", file=sys.stderr) existing_index = build_existing_index(dest_root) migrated = 0 conflicts = 0 review_pkgs = [] errors = [] deleted = 0 merged_subpkgs = 0 hooks_count = 0 for idx, (cat, name, pkgdir, pkgfile) in enumerate(entries, 1): target_cat = CATEGORY_MAP.get(cat, cat) target_dir = os.path.join(dest_root, target_cat, name) left_to_do = total_entries - idx # Optymalizacja odświeżania terminala - odświeżaj co 20 plików, chyba że to ostatni if not args.verbose and not args.dry_run: if idx % 20 == 0 or idx == total_entries: print(f"\r⏳ [{idx}/{total_entries}] Buduje: {cat}/{name} | Zostało: {left_to_do}", end="\033[K", flush=True) try: with open(pkgfile, encoding="utf-8", errors="replace") as f: text = f.read() scalars, arrays, functions = parse_pkgfile(text) doc, review, hook_files, merged = build_yaml( pkgdir, scalars, arrays, functions, known_names=known_names) except Exception as e: errors.append((cat, name, str(e))) if args.verbose: print(f" ⚠️ BŁĄD {cat}/{name}: {e}", file=sys.stderr) elif not args.dry_run: print(f"\n ⚠️ BŁĄD {cat}/{name}: {e}", file=sys.stderr) continue if args.brand: apply_brand(doc) yaml_text = dump_yaml(doc) if review: review_pkgs.append((cat, name, review)) if merged: merged_subpkgs += 1 if doc.get("hooks"): hooks_count += 1 if args.dry_run: print(f"[dry-run] [{idx}/{total_entries}] {cat}/{name} -> {target_cat}/{name}/PAGBUILD.yaml" + (f" ⚠️ {'; '.join(review)}" if review else "")) continue # Sprawdzanie konfliktów ze zbuforowanego słownika existing = existing_index.get(name) if existing and os.path.abspath(existing) != os.path.abspath(target_dir): conflicts += 1 if args.verbose or args.dry_run: print(f"⚠️ Konflikt: {name} istnieje też w {os.path.relpath(existing, dest_root)}", file=sys.stderr) else: print(f"\n⚠️ Konflikt: {name} istnieje też w {os.path.relpath(existing, dest_root)}", file=sys.stderr) dst_file = os.path.join(target_dir, "PAGBUILD.yaml") if args.no_overwrite and os.path.isfile(dst_file): if args.verbose: print(f"⏭ Pomijam (istnieje): {cat}/{name} -> {target_cat}/{name}") continue os.makedirs(target_dir, exist_ok=True) with open(dst_file, "w", encoding="utf-8") as f: f.write(yaml_text) # Rejestrujemy nowy pakiet w indeksie, na wypadek duplikatów w dalszej części przetwarzania existing_index[name] = target_dir for item in os.listdir(pkgdir): if item == "Pkgfile" or item.endswith(".footprint"): # Pkgfile -> PAGBUILD.yaml; *.footprint celowo pomijane (model # jak Arch/Debian — bez deklarowanych manifestów plików). continue if item in hook_files: # hooki są osadzone w polu hooks: PAGBUILD.yaml continue src = os.path.join(pkgdir, item) dst = os.path.join(target_dir, item) if os.path.isfile(src): shutil.copy2(src, dst) elif os.path.isdir(src): if os.path.exists(dst): shutil.rmtree(dst) shutil.copytree(src, dst) migrated += 1 if args.verbose: flag = " ⚠️ " + "; ".join(review) if review else "" print(f" ✅ [{idx}/{total_entries}] {cat}/{name} -> {target_cat}/{name}{flag}") if args.delete_source: pkg_removed = True shutil.rmtree(pkgdir) deleted += 1 if not args.verbose and not args.dry_run: print() print("\n" + "=" * 70) print(f"Migrowano: {migrated}") print(f"Konflikty nazw: {conflicts}") print(f"Błędy: {len(errors)}") print(f"Usunięto źródeł: {deleted}") print(f"Subpakiety scalone (PKGMK_GROUPS): {merged_subpkgs} (zgodnie z formatem pagbuild)") print(f"Hooki pre/post-install/remove: {hooks_count}") if review_pkgs: print(f"\n⚠️ Wymaga przeglądu ({len(review_pkgs)}):") for cat, name, rs in review_pkgs: print(f" - {cat}/{name}: {'; '.join(rs)}") if errors: print("\n❌ Błędy parsowania (pominięte):") for cat, name, msg in errors: print(f" - {cat}/{name}: {msg}") if args.dry_run: print("\n(dry-run: nic nie zapisano ani nie usunięto)") if __name__ == "__main__": main()