feat: support ARMv6 glibc upgrade bundles

This commit is contained in:
whyour
2026-10-18 01:50:59 +08:00
parent c8df528123
commit 24b8be04c9
7 changed files with 123 additions and 4 deletions
+19 -2
View File
@@ -575,7 +575,10 @@ jobs:
build-upgrade-bundles:
needs: [build-alpine, build-debian]
runs-on: ubuntu-22.04
timeout-minutes: 45
timeout-minutes: 90
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
@@ -588,6 +591,7 @@ jobs:
- { platform: linux/s390x, libc: musl, name: s390x-musl }
- { platform: linux/386, libc: musl, name: ia32-musl }
- { platform: linux/amd64, libc: glibc, name: x64-glibc }
- { platform: linux/arm/v6, libc: glibc, name: armv6-glibc }
- { platform: linux/arm/v7, libc: glibc, name: armv7-glibc }
- { platform: linux/arm64, libc: glibc, name: arm64-glibc }
- { platform: linux/ppc64le, libc: glibc, name: ppc64-glibc }
@@ -602,6 +606,7 @@ jobs:
env:
ALPINE_DIGEST: ${{ needs.build-alpine.outputs.image-digest }}
DEBIAN_DIGEST: ${{ needs.build-debian.outputs.image-digest }}
GH_TOKEN: ${{ github.token }}
BUNDLE_LIBC: ${{ matrix.libc }}
BUNDLE_PLATFORM: ${{ matrix.platform }}
run: |
@@ -609,7 +614,19 @@ jobs:
digest="$ALPINE_DIGEST"
if [ "$BUNDLE_LIBC" = glibc ]; then digest="$DEBIAN_DIGEST"; fi
test -n "$digest"
docker buildx build --platform "$BUNDLE_PLATFORM" --build-arg "BASE_IMAGE=ghcr.io/${GITHUB_REPOSITORY}@${digest}" --file docker/Dockerfile.upgrade-bundle --output type=local,dest=output/upgrade .
image="ghcr.io/${GITHUB_REPOSITORY}@${digest}"
if [ "$BUNDLE_PLATFORM/$BUNDLE_LIBC" = linux/arm/v6/glibc ]; then
# This amd64 helper executes a genuine Raspbian ARMv6 userspace under
# ARM1176 QEMU. It is a CI validator, not a user runtime or bundle.
echo "$GH_TOKEN" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin
validator="ghcr.io/${GITHUB_REPOSITORY}-armv6-glibc-validator:${GITHUB_SHA}"
mkdir -p output/validator
docker buildx build --platform linux/amd64 --build-arg "SOURCE_IMAGE=$image" --file docker/Dockerfile.armv6-glibc --target runtime --tag "$validator" --push --metadata-file output/validator/image.json .
validator_digest="$(jq -er '."containerimage.digest"' output/validator/image.json)"
docker buildx build --platform linux/amd64 --build-arg "BASE_IMAGE=${validator%:*}@${validator_digest}" --file docker/Dockerfile.upgrade-bundle-armv6-glibc --output type=local,dest=output/upgrade .
else
docker buildx build --platform "$BUNDLE_PLATFORM" --build-arg "BASE_IMAGE=$image" --file docker/Dockerfile.upgrade-bundle --output type=local,dest=output/upgrade .
fi
- uses: actions/upload-artifact@v6
with:
name: upgrade-${{ matrix.name }}-${{ github.sha }}
+38
View File
@@ -0,0 +1,38 @@
# ARMv7 is a source of application files only. None of its system libraries or
# Node binaries are executed or copied into the ARMv6 runtime.
ARG SOURCE_IMAGE
FROM --platform=linux/arm/v7 ${SOURCE_IMAGE} AS source
# PRoot keeps ARM1176 execution isolated, without installing host binfmt handlers.
FROM --platform=linux/amd64 debian:bookworm-slim@sha256:7c7b2c966bc9ee8cedfeef67e0e279108992c77681fa595db4a9d65c06ccc587 AS runtime
COPY --from=source /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
RUN sed -i 's|http://deb.debian.org|https://deb.debian.org|g' /etc/apt/sources.list.d/debian.sources && \
apt-get update && apt-get install -y --no-install-recommends \
debootstrap gpg gpgv ca-certificates curl qemu-user-static proot && \
rm -rf /var/lib/apt/lists/*
RUN curl -fsSL https://archive.raspbian.org/raspbian.public.key -o /tmp/raspbian.key && \
echo 'ca59cd4f2bcbc3a1d41ba6815a02a8dc5c175467a59bd87edeac458f4a5345de /tmp/raspbian.key' | sha256sum -c - && \
gpg --dearmor -o /usr/share/keyrings/raspbian.gpg /tmp/raspbian.key && \
debootstrap --foreign --arch=armhf --variant=minbase \
--keyring=/usr/share/keyrings/raspbian.gpg \
bullseye /armv6 https://archive.raspbian.org/raspbian
COPY docker/armv6-run /usr/local/bin/armv6-run
RUN chmod 755 /usr/local/bin/armv6-run && \
armv6-run /debootstrap/debootstrap --second-stage && \
cp /usr/share/keyrings/raspbian.gpg /armv6/usr/share/keyrings/raspbian.gpg && \
printf '%s\n' 'deb [signed-by=/usr/share/keyrings/raspbian.gpg] https://archive.raspbian.org/raspbian bullseye main' > /armv6/etc/apt/sources.list && \
mkdir -p /armv6/etc/ssl/certs && \
cp /etc/ssl/certs/ca-certificates.crt /armv6/etc/ssl/certs/ca-certificates.crt && \
armv6-run /bin/sh -ec 'apt-get update; DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends build-essential binutils python3 ca-certificates curl git nginx procps jq sudo; rm -rf /var/lib/apt/lists/*'
# This Node is a pinned CI/test prerequisite, never part of the upgrade archive.
# Build against the minimum supported Node, including its bundled C++ headers.
RUN curl -fsSL https://unofficial-builds.nodejs.org/download/release/v20.18.1/node-v20.18.1-linux-armv6l.tar.xz -o /tmp/node.tar.xz && \
echo '02f4c973a1e432d91f8509b5268d26f3a10f4af33312907d9f43ba2483f14d34 /tmp/node.tar.xz' | sha256sum -c - && \
tar -xJf /tmp/node.tar.xz -C /armv6/usr/local --strip-components=1 && rm /tmp/node.tar.xz
COPY --from=source /ql/ /armv6/ql/
# PM2 is JavaScript. Keep the ARMv6 Node/npm and the Raspbian system libraries.
COPY --from=source /usr/local/lib/node_modules/pm2/ /armv6/usr/local/lib/node_modules/pm2/
RUN ln -s ../lib/node_modules/pm2/bin/pm2 /armv6/usr/local/bin/pm2 && \
armv6-run /bin/bash -ec 'ulimit -c 0; cd "$(readlink -f /ql/node_modules/sqlite3)"; CFLAGS="-march=armv6 -mfpu=vfp -mfloat-abi=hard" CXXFLAGS="-march=armv6 -mfpu=vfp -mfloat-abi=hard" npm_config_nodedir=/usr/local npm_config_build_from_source=true node --jitless -e '\''require("node:child_process").execFileSync(process.execPath,[require.resolve("@mapbox/node-pre-gyp/bin/node-pre-gyp"),"rebuild","--napi_build_version=6"],{stdio:"inherit"})'\''; readelf -A lib/binding/napi-v6-linux-arm/node_sqlite3.node | grep -E "Tag_CPU_arch: v6($|K)"; readelf -A lib/binding/napi-v6-linux-arm/node_sqlite3.node | grep "Tag_ABI_VFP_args: VFP registers"; rm -rf build; node --jitless -e '\''const p=process; if(p.config.variables.arm_version!=="6" && p.config.variables.arm_version!==6) process.exit(1); console.log(JSON.stringify({node:p.version,arm:p.config.variables.arm_version,float:p.config.variables.arm_float_abi,glibc:p.report.getReport().header.glibcVersionRuntime}));'\'''
LABEL qinglong.qa.runtime="armv6-glibc-arm1176"
ENTRYPOINT ["/usr/local/bin/armv6-run"]
@@ -0,0 +1,13 @@
# An immutable digest of the CI-only ARM1176 validator, built in the same run.
ARG BASE_IMAGE
FROM ${BASE_IMAGE} AS bundle
ARG BASE_IMAGE
ENV QL_BUNDLE_BASE_IMAGE=${BASE_IMAGE}
RUN ulimit -c 0; armv6-run /bin/bash -ec \
'node --jitless /ql/scripts/build-upgrade-bundle.cjs /ql /out; \
for archive in /out/*.tar.gz; do \
node /ql/shell/install-bundle.cjs --root /tmp/ql-bundle-offline-smoke --file "$archive" --install; \
rm -rf /tmp/ql-bundle-offline-smoke; \
done'
FROM scratch
COPY --from=bundle /armv6/out/ /
+7
View File
@@ -0,0 +1,7 @@
#!/bin/sh
set -eu
export PROOT_NO_SECCOMP=1 QEMU_CPU=arm1176
# Bind only kernel interfaces; all application files and writable state remain
# inside this container's ARMv6 root. Child processes also pass through QEMU.
exec proot -0 -q '/usr/bin/qemu-arm-static -cpu arm1176' \
-r /armv6 -b /proc -b /dev -w / "$@"
+4 -1
View File
@@ -8,7 +8,7 @@ CI 从同次构建的不可变镜像摘要导出完整 tar.gz,包含源码中
面板下载任务与本地离线入口共用 `platformBundle.cjs`:平台匹配 → 归档安全检查 → 全文件/链接/权限和锁文件校验 → SQLite/Sequelize/cron-parser/gRPC/undici 加载及 SQLite 内存库读写 → 发布就绪指针。重启前重新验证完整载荷。离线升级同步面板任务状态,并与在线任务互斥;中断准备时保留之前的可用就绪包。原有服务停止、文件备份、启动健康检查及失败恢复仍由共享升级引擎处理。
渠道索引为 GitHub prerelease `upgrade-develop` 或 `upgrade-master` 下的 `latest.json`。索引记录准确字节数、SHA256 和不可变 `upgrade-<commit>` release 资产 URL。12 个平台均完成归档、解包后复验及依赖试运行才发布渠道;过期工作流不能覆盖更新提交的渠道。
渠道索引为 GitHub prerelease `upgrade-develop` 或 `upgrade-master` 下的 `latest.json`。索引记录准确字节数、SHA256 和不可变 `upgrade-<commit>` release 资产 URL。13 个平台均完成归档、解包后复验及依赖试运行才发布渠道;过期工作流不能覆盖更新提交的渠道。
## 手动离线安装和升级
@@ -36,11 +36,14 @@ QL_DIR=/opt/qinglong bash /opt/qinglong/shell/start-simplify.sh
`--install` 拒绝非空目录,不覆盖既有配置。该包安装青龙及其 Node 生产依赖;首次启动仍需已有 Python、Bash、PM2、nginx 等相应运行环境,不会在线安装或替换它们。服务启动和系统自启按原手动安装流程配置。
ARMv6 glibc 小设备(例如 Raspberry Pi 1/Zero 的 Raspbian/Raspberry Pi OS 32 位系统)选择 `linux-armv6-glibc.tar.gz`,以同一安装器执行上述离线命令。该包要求 glibc >= 2.31、ARMv6 硬浮点 Node >= 20.18.1,不适用于 ARMv5、软浮点 Node 或旧版 glibc。安装和升级均不需要 Docker,也不会下载或替换 Node。
## 平台兼容
- 最低 Node 为 20.18.1,Node-API 至少 6;安装器只检查,不替换 Node。
- 以 Node 实际运行架构、ARM 指令集和 libc 类型/版本选择包;不依据 Docker 架构别名猜测。
- glibc、musl 分包,并记录构建系统 libc 下限。系统库仍以目标设备加载测试作为最后检查。
- ARMv6 glibc 使用签名校验的 Raspbian bullseye(glibc 2.31)构建环境,以固定 SHA256 的 ARMv6 Node 20.18.1 头文件编译 SQLite,强制 `-march=armv6 -mfpu=vfp -mfloat-abi=hard`。CI 检查 ELF 的 ARMv6 和硬浮点标记,并在 QEMU ARM1176 下完成归档复验、SQLite 读写和空目录离线安装;验证辅助镜像按不可变摘要记录,不进入用户安装包。Debian ARMv7 镜像仅提供同次构建的应用文件,不复用其 Node、libc 或 SQLite 二进制。
- ARMv6 与 ARMv7 使用不同包名。ARMv6 在目标镜像中从源码编译 SQLite,禁止复用 generic `arm` 的 ARMv7 预编译件;构建需通过加载和内存库读写,失败则不发布渠道。
- ARMv6 的归档构建、在线更新工作进程及独立离线安装器使用 `--jitless`,避开 ARM1176 上 V8 的优化编译崩溃;保留现有 Node 二进制,正常面板进程的启动参数不变。依赖加载和 SQLite 读写仍由现有 Node 的独立进程验证。
- tar 中允许 pnpm 的包内符号链接;拒绝绝对路径、越界路径、包外链接、硬链接、设备条目和经符号链接写入的条目。重启前再次校验依赖,暂存失败保留上一次可用的就绪包。
+1
View File
@@ -13,6 +13,7 @@ const platforms = [
'linux-s390x-musl',
'linux-ia32-musl',
'linux-x64-glibc',
'linux-armv6-glibc',
'linux-armv7-glibc',
'linux-arm64-glibc',
'linux-ppc64-glibc',
+41 -1
View File
@@ -202,6 +202,35 @@ test('native target compatibility distinguishes ARMv6/ARMv7 and libc and retains
target: { ...target, libcMinimum: '1.2.4' },
};
bundle.assertCompatible(manifest, target, '20.20.2');
const glibcTarget = {
...target,
libc: 'glibc',
libcVersion: '2.31',
endianness: 'LE',
armFloatAbi: 'hard',
};
const glibcManifest = {
...manifest,
target: { ...glibcTarget, libcMinimum: '2.31' },
};
bundle.assertCompatible(glibcManifest, glibcTarget, '20.18.1');
assert.equal(bundle.targetKey(glibcTarget), 'linux-armv6-glibc');
assert.throws(
() =>
bundle.assertCompatible(glibcManifest, {
...glibcTarget,
libcVersion: '2.30',
}),
/requires glibc/,
);
assert.throws(
() =>
bundle.assertCompatible(glibcManifest, {
...glibcTarget,
armFloatAbi: 'soft',
}),
/floating-point ABI mismatch/,
);
assert.throws(
() => bundle.assertCompatible(manifest, { ...target, arch: 'armv7' }),
/platform mismatch/,
@@ -309,7 +338,18 @@ test('publishing requires every platform and validates all immutable archive has
);
}
const index = releaseIndex(f.directory, sourceCommit, repository);
assert.equal(Object.keys(index.packages).length, 12);
assert.equal(Object.keys(index.packages).length, 13);
const armv6 = path.join(
f.directory,
`qinglong-${sourceCommit}-linux-armv6-glibc.tar.gz`,
);
const armv6Bytes = await fs.readFile(armv6);
await fs.rm(armv6);
assert.throws(
() => releaseIndex(f.directory, sourceCommit, repository),
/ENOENT/,
);
await fs.writeFile(armv6, armv6Bytes);
await fs.appendFile(
path.join(f.directory, `qinglong-${sourceCommit}-${platforms[0]}.tar.gz`),
'corrupt',