#!/usr/bin/env python3 """3X-UI_KIT (форк hacker07): накатывает наши «швы» на чистый код автора. Это источник правды для форка: единственное место, где описано, ЧТО именно мы правим в файлах автора. Всё, что мы дописываем по существу, живёт в scripts/kit-local.sh — файле, который автор не трогает, поэтому обновления автора его не ломают. Правила: * каждый якорь обязан найтись РОВНО один раз, иначе — стоп (автор изменил код, правку надо пересмотреть руками); * при любой ошибке ничего не пишется наполовину: сначала прогоняй --check; * открытый ключ подписи берётся из tools/fork-signer.pub (закрытая часть — только на рабочей станции владельца, в репозиторий не попадает). Использование: python3 tools/fork_apply.py --check [корень-репозитория] python3 tools/fork_apply.py [корень-репозитория] """ import os import pathlib import re import sys GITEA = os.environ.get("FORK_GITEA_BASE", "https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS") SIGNER_FILE = os.environ.get("FORK_SIGNER_PUB", "tools/fork-signer.pub") # Шапка авторского README: человек, открывший репозиторий, должен сразу видеть, что это форк # и что ставить надо из него, а не по команде автора ниже (та включает авторский канал обновлений). README_BLOCK = """> **Это форк — сервер ставится и обновляется из этого репозитория, а не с GitHub.** > Установка одной командой (подробности и флаги — в [FORK.md](FORK.md)): > `bash <(curl -fsSL https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS/raw/branch/main/scripts/3x-ui-local.sh) --domain vpn.example.com` > Свои команды `kit local …` и записи о версиях — [FORK.md](FORK.md), [FORK-CHANGELOG.md](FORK-CHANGELOG.md). > Ниже — **авторское** описание: его команды и значки ведут в репозиторий автора и включают его > канал обновлений, то есть ночное автообновление вернёт авторскую сборку вместо нашей. """ README_HEAD = '
\n\n3X-UI KIT' HOOK = """ # --- 3X-UI_KIT (форк hacker07): локальные команды и хуки --- # Подключаем scripts/kit-local.sh, если он установлен. Наш файл: правки автора его не касаются. # Все команды форка — под «kit local …» (см. kit local help). KIT_LOCAL_LIB="${KIT_LOCAL_LIB:-/usr/local/lib/kit/kit-local.sh}" if [[ -r $KIT_LOCAL_LIB ]]; then . "$KIT_LOCAL_LIB"; kit_local_dispatch "$@"; fi """ def die(msg): print("! " + msg, file=sys.stderr) sys.exit(1) def ok(msg): print("==> " + msg) def count(text, anchor, regex): if regex: return len(re.findall(anchor, text, re.MULTILINE)) return text.count(anchor) def main(): args = [a for a in sys.argv[1:]] check = False if "--check" in args: check = True args.remove("--check") root = pathlib.Path(args[0] if args else ".").resolve() if not (root / "VERSION").exists(): die("%s не похож на репозиторий 3X-UI KIT (нет VERSION)" % root) os.chdir(root) version = (root / "VERSION").read_text(encoding="utf-8").strip() if not re.fullmatch(r"[0-9]+(\.[0-9]+)+", version): die("VERSION=%r: нужен числовой номер вида 1.2.1" % version) signer_path = pathlib.Path(SIGNER_FILE) if not signer_path.exists(): die( "нет %s — сначала создай ключ подписи:\n" " ssh-keygen -t ed25519 -C \"3X-UI KIT fork releases\" -f ~/.ssh/3x-ui-kit-fork/release_key\n" " mkdir -p tools && ssh-keygen -y -f ~/.ssh/3x-ui-kit-fork/release_key > tools/fork-signer.pub" % SIGNER_FILE ) pub = "" for line in signer_path.read_text(encoding="utf-8").splitlines(): line = line.strip() if line.startswith(("ssh-ed25519 ", "ssh-rsa ")): pub = line break if not pub: die("%s: не нашёл строку открытого ключа (ssh-ed25519 …)" % signer_path) version_re = r'^KIT_VERSION="[0-9.]+"$' edits = [ # --- scripts/kit.sh ------------------------------------------------- ("scripts/kit.sh", version_re, True, 'KIT_VERSION="%s"' % version, "версия сборки форка"), ( "scripts/kit.sh", 'KIT_RAW="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main"', False, 'KIT_RAW="%s/raw/branch/main"' % GITEA, "источник VERSION и подсказки об обновлении", ), ( "scripts/kit.sh", 'kit_ref_raw() { echo "https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v$1"; }', False, 'kit_ref_raw() { echo "%s/raw/tag/v$1"; }' % GITEA + "\n" + HOOK, "файлы релиза из нашего тега + подключение kit-local.sh", ), ( "scripts/kit.sh", r' # SHA256:[^\n]*\n "ssh-ed25519 [^\n]*"\n', True, ' # Ключ подписи форка (владелец: hacker07). Закрытая часть — только на рабочей станции.\n' ' "%s"\n' % pub, "ключ проверки подписи релизов", ), ( "scripts/kit.sh", "for f in scripts/kit.sh scripts/kit-sub.py; do", False, "for f in scripts/kit.sh scripts/kit-sub.py scripts/kit-local.sh; do", "наш файл в списке проверяемых и скачиваемых", ), ( "scripts/kit.sh", ' install -m 755 "$tmp/kit.sh" /usr/local/bin/kit.new && mv -f /usr/local/bin/kit.new /usr/local/bin/kit', False, ' install -m 755 "$tmp/kit.sh" /usr/local/bin/kit.new && mv -f /usr/local/bin/kit.new /usr/local/bin/kit\n' " # Форк: ставим наши локальные дополнения из того же подписанного релиза.\n" " install -d -m 755 /usr/local/lib/kit\n" ' install -m 644 "$tmp/kit-local.sh" /usr/local/lib/kit/kit-local.sh', "установка kit-local.sh при обновлении", ), # --- scripts/3x-ui.sh ----------------------------------------------- ("scripts/3x-ui.sh", version_re, True, 'KIT_VERSION="%s"' % version, "версия сборки форка"), ( "scripts/3x-ui.sh", 'KIT_RAW="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v$KIT_VERSION"', False, 'KIT_RAW="%s/raw/tag/v$KIT_VERSION"' % GITEA, "запасная загрузка kit из нашего релиза", ), ( "scripts/3x-ui.sh", 'KIT_CLI_URL="$KIT_RAW/scripts/kit.sh"', False, 'KIT_CLI_URL="$KIT_RAW/scripts/kit.sh"\nKIT_LOCAL_URL="$KIT_RAW/scripts/kit-local.sh"', "адрес нашего файла для установщика", ), ( "scripts/3x-ui.sh", 'KIT_INSTALL_CMD="bash <(curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main/scripts/3x-ui.sh)"', False, 'KIT_INSTALL_CMD="bash <(curl -fsSL %s/raw/branch/main/scripts/3x-ui.sh)"' % GITEA, "команда установки, которую показывает панель", ), ( "scripts/3x-ui.sh", ' bash -n /usr/local/bin/kit || die "Команда kit скачалась повреждённой"', False, " # Форк: ставим наши локальные дополнения (см. scripts/kit-local.sh).\n" " install -d -m 755 /usr/local/lib/kit\n" ' if [[ -n $src && -f $d/kit-local.sh ]]; then\n' ' install -m 644 "$d/kit-local.sh" /usr/local/lib/kit/kit-local.sh\n' " else\n" ' curl -fsSL --retry 3 -o /usr/local/lib/kit/kit-local.sh "$KIT_LOCAL_URL" \\\n' ' || warn "Локальные дополнения kit не скачались: kit local не будет работать"\n' " fi\n" ' bash -n /usr/local/bin/kit || die "Команда kit скачалась повреждённой"', "установка kit-local.sh установщиком", ), # --- scripts/hysteria2.sh ------------------------------------------- ("scripts/hysteria2.sh", version_re, True, 'KIT_VERSION="%s"' % version, "версия сборки форка"), # --- tools/release.sh ----------------------------------------------- ( "tools/release.sh", "FILES=(scripts/3x-ui.sh scripts/hysteria2.sh scripts/kit.sh scripts/kit-sub.py VERSION)", False, "FILES=(scripts/3x-ui.sh scripts/hysteria2.sh scripts/kit.sh scripts/kit-sub.py scripts/kit-local.sh scripts/3x-ui-local.sh VERSION)", "наш файл в подписанном списке релиза", ), ( "tools/release.sh", '{ echo "# 3X-UI KIT $v"; sha "${FILES[@]}"; } >SHA256SUMS', False, "{ echo \"# 3X-UI KIT $v\"; sha \"${FILES[@]}\" | awk '{ sub(/^\\*/, \"\", $2); print $1\" \"$2 }'; } >SHA256SUMS", "суммы в POSIX-виде без звёздочки (на Windows sha256sum её ставит, и kit её не понимает)", ), # --- шапка авторского README (чтобы ссылку ниже не приняли за нашу) -------------- ( "README.md", README_HEAD, False, README_BLOCK + README_HEAD, "предупреждение в шапке README: это форк, ставить из него", ), # --- свои записи о версии (наш файл, авторский CHANGELOG не трогаем) ---- ( "tools/release.sh", 'grep -q "^## v$v" CHANGELOG.md || die "В CHANGELOG.md нет раздела ## v$v"', False, '(grep -q "^## v$v" CHANGELOG.md || grep -q "^## v$v" FORK-CHANGELOG.md)' ' || die "Нет раздела ## v$v ни в CHANGELOG.md, ни в FORK-CHANGELOG.md (форк ведёт свои записи в FORK-CHANGELOG.md)"', "раздел версии форка — в нашем файле", ), ( "tools/release.sh", '[[ -z $(git status --porcelain -- "${FILES[@]}" CHANGELOG.md) ]]', False, '[[ -z $(git status --porcelain -- "${FILES[@]}" CHANGELOG.md FORK-CHANGELOG.md) ]]', "наш файл записей тоже должен быть закоммичен", ), ( "scripts/kit.sh", ' curl -fsS -m 5 "$KIT_RAW/CHANGELOG.md" 2>/dev/null | awk -v v="## v$1" \'', False, ' { curl -fsS -m 5 "$KIT_RAW/CHANGELOG.md" 2>/dev/null; curl -fsS -m 5 "$KIT_RAW/FORK-CHANGELOG.md" 2>/dev/null; } | awk -v v="## v$1" \'', "«что нового» ищет и в наших записях", ), ] bad = 0 for path, anchor, is_re, new, why in edits: p = pathlib.Path(path) if not p.exists(): print("✗ %s: файла нет" % path) bad += 1 continue text = p.read_text(encoding="utf-8", newline="") if check: # --check смотрит на готовое дерево dev: наш шов обязан быть на месте и ровно один раз n = text.count(new) what, shown = "наш шов", new else: # применение — только на чистое дерево автора: якорь обязан найтись ровно один раз n = count(text, anchor, is_re) what, shown = "якорь автора", anchor short = shown if len(shown) <= 54 else shown[:51] + "…" short = short.replace("\n", "⏎") if n != 1: print("✗ %-20s %-56s %s найден %d раз (%s)" % (path, short, what, n, why)) bad += 1 continue if check: print("✓ %-20s %-56s (%s)" % (path, short, why)) continue if is_re: text = re.sub(anchor, new, text, count=1, flags=re.MULTILINE) else: text = text.replace(anchor, new, 1) p.write_text(text, encoding="utf-8", newline="") print("✓ %-20s %-56s (%s)" % (path, short, why)) if bad: if check: die("якорей не найдено: %d — автор изменил код, пересмотри правки в tools/fork_apply.py" % bad) die("якорей не найдено: %d — правки не применены" % bad) if check: ok("все якоря на месте (версия сборки %s)" % version) else: ok("швы накатаны, версия сборки %s" % version) if __name__ == "__main__": main()