Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 41 additions & 61 deletions scripts/build-apt-repo.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
#!/usr/bin/env bash
# 从 dist/ 目录中的 .deb 构建标准 apt 仓库,输出到 apt-repo/。
# 用法:scripts/build-apt-repo.sh <version> [deb_dir] [out_dir]
# 用法:scripts/build-apt-repo.sh <version> [deb_dir] [out_dir] [repo_slug]
#
# .deb 文件不存入 gh-pages(避免 git 历史膨胀)。
# Packages 索引的 Filename 字段直接指向 GitHub Releases 下载地址,
# apt 客户端会从该 URL 拉取 .deb。
#
# 产物结构(部署到 GitHub Pages 的 /debian/ 路径):
# pool/main/b/bash-agent/*.deb
# dists/stable/main/binary-{amd64,arm64,all}/Packages(.gz)
# dists/stable/main/binary-{amd64,arm64}/Packages(.gz)
# dists/stable/Release (+ InRelease, 若提供 GPG_PRIVATE_KEY)
#
# 索引由内嵌 Python 生成(跨平台一致,不依赖 apt-ftparchive)。
Expand All @@ -14,76 +17,55 @@ set -euo pipefail
VERSION_INPUT="${1:-}"
DEB_DIR="${2:-dist}"
OUT_DIR="${3:-apt-repo}"
REPO_SLUG="${4:-lloydzhou/bash-agent}"

if [[ -z "$VERSION_INPUT" ]];then
echo "用法:$0 <version> [deb_dir] [out_dir]" >&2
echo "用法:$0 <version> [deb_dir] [out_dir] [repo_slug]" >&2
exit 2
fi
VERSION="${VERSION_INPUT#v}"

[[ -d "$DEB_DIR" ]] || { echo "错误:deb 目录不存在:$DEB_DIR" >&2; exit 1; }

# GitHub Releases 下载基础 URL(apt 从这里拉 .deb,不经过 gh-pages)
RELEASES_URL="https://github.com/${REPO_SLUG}/releases/download/${VERSION_INPUT}"

REPO_DIR="$OUT_DIR/debian"
POOL_DIR="$REPO_DIR/pool/main/b/bash-agent"
rm -rf "$OUT_DIR"
mkdir -p "$POOL_DIR"
mkdir -p "$REPO_DIR/dists/stable"

# 1. 收集 .deb 到 pool(只收架构包;防御性排除 *_all.deb,避免 apt 选择纯 Bash 包
# 收集 .deb 文件名(只收架构包)
shopt -s nullglob
debs=( "$DEB_DIR"/bash-agent_*_amd64.deb "$DEB_DIR"/bash-agent_*_arm64.deb )
shopt -u nullglob
if (( ${#debs[@]} == 0 ));then
echo "错误:$DEB_DIR 中没有架构 .deb 文件(*_amd64.deb / *_arm64.deb)" >&2
exit 1
fi
for deb in "${debs[@]}"; do
echo "收录:$(basename "$deb")"
cp "$deb" "$POOL_DIR/"
done

# 2/3. 生成 Packages 索引与 Release 文件
# 优先使用 apt-ftparchive(官方工具,格式标准);无则退回 Python fallback
if command -v apt-ftparchive >/dev/null 2>&1; then
for arch in amd64 arm64; do
bindir="$REPO_DIR/dists/stable/main/binary-$arch"
mkdir -p "$bindir"
( cd "$REPO_DIR" && apt-ftparchive packages --arch "$arch" pool/main ) > "$bindir/Packages"
gzip -9n -c "$bindir/Packages" > "$bindir/Packages.gz"
echo "生成:dists/stable/main/binary-$arch/Packages.gz ($(grep -c '^Package:' "$bindir/Packages") 条)"
done
( cd "$REPO_DIR" && apt-ftparchive release \
-o APT::FTPArchive::Release::Origin=bash-agent \
-o APT::FTPArchive::Release::Label=bash-agent \
-o APT::FTPArchive::Release::Suite=stable \
-o APT::FTPArchive::Release::Codename=stable \
-o APT::FTPArchive::Release::Architectures="amd64 arm64" \
-o APT::FTPArchive::Release::Components=main \
-o APT::FTPArchive::Release::Description="bash-agent APT repository" \
dists/stable ) > "$REPO_DIR/dists/stable/Release"
echo "生成:dists/stable/Release(apt-ftparchive)"
else
echo "警告:无 apt-ftparchive,使用 Python 生成索引" >&2
PYTHON_BIN="$(command -v python3 || command -v python)"
export REPO_DIR VERSION
"$PYTHON_BIN" - <<'PYEOF'
import gzip, hashlib, os, re, subprocess, sys

repo = os.environ["REPO_DIR"]
version = os.environ["VERSION"]
pool = os.path.join(repo, "pool/main/b/bash-agent")
comp = "main"
dist = os.path.join(repo, "dists/stable")

debs = sorted(f for f in os.listdir(pool) if f.endswith(".deb"))

# 生成 Packages 索引(Python,跨平台一致)
# Filename 字段指向 GitHub Releases URL,.deb 不写入 gh-pages
PYTHON_BIN="$(command -v python3 || command -v python)"
export REPO_DIR VERSION RELEASES_URL DEB_DIR

"$PYTHON_BIN" - <<'PYEOF'
import gzip, hashlib, os, io, tarfile, sys, datetime

repo = os.environ["REPO_DIR"]
version = os.environ["VERSION"]
releases = os.environ["RELEASES_URL"]
deb_dir = os.environ["DEB_DIR"]
comp = "main"
dist = os.path.join(repo, "dists/stable")

debs = sorted(f for f in os.listdir(deb_dir) if f.endswith(".deb") and "_all" not in f)
if not debs:
sys.exit("错误:pool 中没有 .deb")
sys.exit("错误:没有架构 .deb")

def control_fields(deb_path):
"""读取 .deb 的 control 字段(ar + tar 解出 control.tar.*)"""
import io, tarfile
with open(deb_path, "rb") as f:
data = f.read()
# ar 归档解析
idx = 8
members = {}
while idx + 60 <= len(data):
Expand Down Expand Up @@ -111,15 +93,16 @@ def control_fields(deb_path):
fields["Description"] += "\n" + line.strip()
return fields

def pkg_entry(relpath):
full = os.path.join(repo, relpath)
def pkg_entry(deb_name):
full = os.path.join(deb_dir, deb_name)
with open(full, "rb") as f:
data = f.read()
ctl = control_fields(full)
size = len(data)
sha256 = hashlib.sha256(data).hexdigest()
sha1 = hashlib.sha1(data).hexdigest()
md5 = hashlib.md5(data).hexdigest()
filename_url = f"{releases}/{deb_name}"
lines = [
f"Package: {ctl.get('Package', 'bash-agent')}",
f"Version: {ctl.get('Version', version)}",
Expand All @@ -133,29 +116,28 @@ def pkg_entry(relpath):
if ctl.get("Description"):
lines.append(f"Description: {ctl['Description'].splitlines()[0]}")
lines += [
f"Filename: {relpath}",
f"Filename: {filename_url}",
f"Size: {size}",
f"MD5sum: {md5}",
f"SHA1: {sha1}",
f"SHA256: {sha256}",
]
return "\n".join(lines) + "\n\n", size, md5, sha1, sha256
return "\n".join(lines) + "\n\n"

# 按架构生成 Packages
entries_by_arch = {}
for deb in debs:
rel = os.path.join("pool/main/b/bash-agent", deb)
entry, size, md5, sha1, sha256 = pkg_entry(rel)
arch = control_fields(os.path.join(repo, rel)).get("Architecture", "amd64")
entries_by_arch.setdefault(arch, []).append((rel, entry, size, md5, sha1, sha256))
entry = pkg_entry(deb)
arch = control_fields(os.path.join(deb_dir, deb)).get("Architecture", "amd64")
entries_by_arch.setdefault(arch, []).append(entry)

archs = sorted(entries_by_arch)
for arch in archs:
bindir = os.path.join(dist, comp, f"binary-{arch}")
os.makedirs(bindir, exist_ok=True)
pkgs_path = os.path.join(bindir, "Packages")
with open(pkgs_path, "w") as f:
for _, entry, *_ in entries_by_arch[arch]:
for entry in entries_by_arch[arch]:
f.write(entry)
with open(pkgs_path + ".gz", "wb") as f:
with open(pkgs_path, "rb") as raw:
Expand All @@ -168,7 +150,6 @@ def file_info(path):
data = f.read()
return len(data), hashlib.md5(data).hexdigest(), hashlib.sha1(data).hexdigest(), hashlib.sha256(data).hexdigest()

import datetime
release_extra = {
"Origin": "bash-agent",
"Label": "bash-agent",
Expand Down Expand Up @@ -202,9 +183,8 @@ with open(os.path.join(dist, "Release"), "w") as f:
print("生成:dists/stable/Release")
print("APT 仓库构建完成:", repo)
PYEOF
fi

# 4. 可选 GPG 签名
# 可选 GPG 签名
if [[ -n "${GPG_PRIVATE_KEY:-}" ]]; then
printf '%s\n' "$GPG_PRIVATE_KEY" | gpg --batch --yes --import 2>/dev/null || \
printf '%s\n' "$GPG_PRIVATE_KEY" | base64 -d 2>/dev/null | gpg --batch --yes --import 2>/dev/null || true
Expand Down
Loading