🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017
#!/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=<typ> -> 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"(?<![A-Za-z0-9])" + re.escape(version) + r"(?![A-Za-z0-9])",
"${pkgver}", s,
)
out.append(s)
return out
def is_url(s: str) -> 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/<release> (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<ver> (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()