#!/usr/bin/env python3 """Package already-built Linux amd64 resources; never include runtime state.""" import argparse import datetime import hashlib import json import subprocess import tarfile import tempfile from pathlib import Path import shutil root = Path(__file__).resolve().parent.parent parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--output-dir', type=Path, default=root) args = parser.parse_args() args.output_dir.mkdir(parents=True, exist_ok=True) (root / '.cache').mkdir(exist_ok=True) def digest(path): return hashlib.sha256(path.read_bytes()).hexdigest() for name in ('clashweb', 'mihomo'): header = (root / 'bin' / name).read_bytes()[:64] if header[:5] != b'\x7fELF\x02' or int.from_bytes(header[18:20], 'little') != 62: raise SystemExit(f'bin/{name} must be a Linux amd64 ELF binary') update_readme = '''# Clash Web 管理程序更新(Linux amd64) 本包更新 Clash Web 管理服务及其嵌入的 Web UI。 无需更新 Mihomo 或规则,也不修改数据库、管理员密码、订阅和 VM 网络设置。 在已部署的目标 VM 上解压并进入本目录,然后执行: ```bash sha256sum -c SHA256SUMS sudo install -m 0755 -o root -g root /usr/local/bin/clashweb /usr/local/bin/clashweb.previous sudo install -m 0755 -o root -g root bin/clashweb /usr/local/bin/clashweb.next sudo mv /usr/local/bin/clashweb.next /usr/local/bin/clashweb sudo systemctl restart clashweb sudo systemctl status clashweb --no-pager ``` 重启后刷新浏览器,确认管理服务正常。独立 Mihomo 内核继续运行;若页面仍显示旧内容,可用 Ctrl+Shift+R 刷新。 需要回退时执行: ```bash sudo install -m 0755 -o root -g root /usr/local/bin/clashweb.previous /usr/local/bin/clashweb.rollback sudo mv /usr/local/bin/clashweb.rollback /usr/local/bin/clashweb sudo systemctl restart clashweb ``` 测试只检查 HTTPS 主页,不承诺流媒体解锁、AI 账号或镜像拉取。 完整说明见 docs/upgrading.md。 ''' deploy_readme = '''# Clash Web Linux amd64 部署包 包含静态 Go 管理程序与 Web UI、Mihomo v1.19.32、已有大陆规则、安装脚本和文档。 在正式 PVE VM 上部署;不包含任何订阅、管理员密码或开发机运行数据。 解压后进入目录,先运行 `sha256sum -c SHA256SUMS`。 首次安装见 docs/deployment.md;Ubuntu/Debian 需要 sudo、python3、iproute2、nftables、ca-certificates。 运行 `sudo scripts/install.sh --target-vm --bind VM_IP:8080`。 VM 固定 IP、转发、防火墙与客户端网关/DNS 按部署文档配置,VM 不运行 DHCP。 已经部署的 VM 推荐使用 clashweb-update-linux-amd64.tar.gz,只替换管理程序。 不要用包内空数据覆盖 /var/lib/clashweb 或 /var/lib/clashweb-core。 ''' scripts = ['install.sh', 'clashweb-control', 'network-vm.py', 'fetch-mihomo.py', 'fetch-rules.py', 'update-rules.sh', 'diagnose-subscription.py'] with tempfile.TemporaryDirectory(prefix='package-', dir=root / '.cache') as temp: for name, full in [('clashweb-linux-amd64', True), ('clashweb-update-linux-amd64', False)]: stage = Path(temp) / name files = ['bin/clashweb'] if full: files += ['bin/mihomo', 'data/mihomo/rules/cn-domain.mrs', 'data/mihomo/rules/cn-ip.mrs'] files += ['scripts/' + script for script in scripts] files += [p.relative_to(root).as_posix() for p in (root / 'deploy').glob('*.service')] files += ['README.md'] files += ['docs/' + document for document in ['api.md', 'development.md', 'deployment.md', 'upgrading.md', 'third-party.md']] else: files += ['docs/upgrading.md'] for relative in files: destination = stage / relative destination.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(root / relative, destination) (stage / 'README-DEPLOY.md' if full else stage / 'README-UPDATE.md').write_text(deploy_readme if full else update_readme) (stage / 'BUILD-INFO.json').write_text(json.dumps({ 'platform': 'linux/amd64', 'goamd64': 'v1', 'cgoEnabled': False, 'createdAt': datetime.datetime.now(datetime.timezone.utc).isoformat(), 'clashwebSha256': digest(root / 'bin/clashweb'), 'mihomoVersion': 'v1.19.32' if full else None, }, indent=2) + '\n') payload = sorted(p for p in stage.rglob('*') if p.is_file()) (stage / 'SHA256SUMS').write_text(''.join(f'{digest(p)} {p.relative_to(stage).as_posix()}\n' for p in payload)) archive = args.output_dir / f'{name}.tar.gz' with tarfile.open(archive, 'w:gz') as tar: for p in sorted(stage.rglob('*')): if p.is_file(): tar.add(p, arcname=name + '/' + p.relative_to(stage).as_posix(), recursive=False) archive.with_suffix(archive.suffix + '.sha256').write_text(f'{digest(archive)} {archive.name}\n') # Verify the actual archive, not just its staging directory. with tarfile.open(archive) as tar: extracted = Path(temp) / (name + '-verify') tar.extractall(extracted, filter='data') checked = subprocess.run(['sha256sum', '-c', 'SHA256SUMS'], cwd=extracted / name, capture_output=True) if checked.returncode: raise SystemExit('Package payload checksum failed') if full: clean = Path(temp) / 'clean-init' initialized = subprocess.run([str(extracted / name / 'bin/clashweb'), '--init', '--data', str(clean / 'app'), '--core-dir', str(extracted / name / 'data/mihomo'), '--mihomo', str(extracted / name / 'bin/mihomo')], capture_output=True) if initialized.returncode: raise SystemExit('Clean package initialization/core validation failed') print(f'{archive}: {archive.stat().st_size:,} bytes, payload checksums verified')