mirror of
https://github.com/whyour/qinglong.git
synced 2026-10-18 18:33:06 +08:00
feat: support ARMv6 glibc upgrade bundles
This commit is contained in:
@@ -575,7 +575,10 @@ jobs:
|
|||||||
build-upgrade-bundles:
|
build-upgrade-bundles:
|
||||||
needs: [build-alpine, build-debian]
|
needs: [build-alpine, build-debian]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
timeout-minutes: 45
|
timeout-minutes: 90
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -588,6 +591,7 @@ jobs:
|
|||||||
- { platform: linux/s390x, libc: musl, name: s390x-musl }
|
- { platform: linux/s390x, libc: musl, name: s390x-musl }
|
||||||
- { platform: linux/386, libc: musl, name: ia32-musl }
|
- { platform: linux/386, libc: musl, name: ia32-musl }
|
||||||
- { platform: linux/amd64, libc: glibc, name: x64-glibc }
|
- { 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/arm/v7, libc: glibc, name: armv7-glibc }
|
||||||
- { platform: linux/arm64, libc: glibc, name: arm64-glibc }
|
- { platform: linux/arm64, libc: glibc, name: arm64-glibc }
|
||||||
- { platform: linux/ppc64le, libc: glibc, name: ppc64-glibc }
|
- { platform: linux/ppc64le, libc: glibc, name: ppc64-glibc }
|
||||||
@@ -602,6 +606,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
ALPINE_DIGEST: ${{ needs.build-alpine.outputs.image-digest }}
|
ALPINE_DIGEST: ${{ needs.build-alpine.outputs.image-digest }}
|
||||||
DEBIAN_DIGEST: ${{ needs.build-debian.outputs.image-digest }}
|
DEBIAN_DIGEST: ${{ needs.build-debian.outputs.image-digest }}
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
BUNDLE_LIBC: ${{ matrix.libc }}
|
BUNDLE_LIBC: ${{ matrix.libc }}
|
||||||
BUNDLE_PLATFORM: ${{ matrix.platform }}
|
BUNDLE_PLATFORM: ${{ matrix.platform }}
|
||||||
run: |
|
run: |
|
||||||
@@ -609,7 +614,19 @@ jobs:
|
|||||||
digest="$ALPINE_DIGEST"
|
digest="$ALPINE_DIGEST"
|
||||||
if [ "$BUNDLE_LIBC" = glibc ]; then digest="$DEBIAN_DIGEST"; fi
|
if [ "$BUNDLE_LIBC" = glibc ]; then digest="$DEBIAN_DIGEST"; fi
|
||||||
test -n "$digest"
|
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
|
- uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: upgrade-${{ matrix.name }}-${{ github.sha }}
|
name: upgrade-${{ matrix.name }}-${{ github.sha }}
|
||||||
|
|||||||
@@ -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/ /
|
||||||
Executable
+7
@@ -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 / "$@"
|
||||||
@@ -8,7 +8,7 @@ CI 从同次构建的不可变镜像摘要导出完整 tar.gz,包含源码中
|
|||||||
|
|
||||||
面板下载任务与本地离线入口共用 `platformBundle.cjs`:平台匹配 → 归档安全检查 → 全文件/链接/权限和锁文件校验 → SQLite/Sequelize/cron-parser/gRPC/undici 加载及 SQLite 内存库读写 → 发布就绪指针。重启前重新验证完整载荷。离线升级同步面板任务状态,并与在线任务互斥;中断准备时保留之前的可用就绪包。原有服务停止、文件备份、启动健康检查及失败恢复仍由共享升级引擎处理。
|
面板下载任务与本地离线入口共用 `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 等相应运行环境,不会在线安装或替换它们。服务启动和系统自启按原手动安装流程配置。
|
`--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 为 20.18.1,Node-API 至少 6;安装器只检查,不替换 Node。
|
||||||
- 以 Node 实际运行架构、ARM 指令集和 libc 类型/版本选择包;不依据 Docker 架构别名猜测。
|
- 以 Node 实际运行架构、ARM 指令集和 libc 类型/版本选择包;不依据 Docker 架构别名猜测。
|
||||||
- glibc、musl 分包,并记录构建系统 libc 下限。系统库仍以目标设备加载测试作为最后检查。
|
- 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 与 ARMv7 使用不同包名。ARMv6 在目标镜像中从源码编译 SQLite,禁止复用 generic `arm` 的 ARMv7 预编译件;构建需通过加载和内存库读写,失败则不发布渠道。
|
||||||
- ARMv6 的归档构建、在线更新工作进程及独立离线安装器使用 `--jitless`,避开 ARM1176 上 V8 的优化编译崩溃;保留现有 Node 二进制,正常面板进程的启动参数不变。依赖加载和 SQLite 读写仍由现有 Node 的独立进程验证。
|
- ARMv6 的归档构建、在线更新工作进程及独立离线安装器使用 `--jitless`,避开 ARM1176 上 V8 的优化编译崩溃;保留现有 Node 二进制,正常面板进程的启动参数不变。依赖加载和 SQLite 读写仍由现有 Node 的独立进程验证。
|
||||||
- tar 中允许 pnpm 的包内符号链接;拒绝绝对路径、越界路径、包外链接、硬链接、设备条目和经符号链接写入的条目。重启前再次校验依赖,暂存失败保留上一次可用的就绪包。
|
- tar 中允许 pnpm 的包内符号链接;拒绝绝对路径、越界路径、包外链接、硬链接、设备条目和经符号链接写入的条目。重启前再次校验依赖,暂存失败保留上一次可用的就绪包。
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const platforms = [
|
|||||||
'linux-s390x-musl',
|
'linux-s390x-musl',
|
||||||
'linux-ia32-musl',
|
'linux-ia32-musl',
|
||||||
'linux-x64-glibc',
|
'linux-x64-glibc',
|
||||||
|
'linux-armv6-glibc',
|
||||||
'linux-armv7-glibc',
|
'linux-armv7-glibc',
|
||||||
'linux-arm64-glibc',
|
'linux-arm64-glibc',
|
||||||
'linux-ppc64-glibc',
|
'linux-ppc64-glibc',
|
||||||
|
|||||||
@@ -202,6 +202,35 @@ test('native target compatibility distinguishes ARMv6/ARMv7 and libc and retains
|
|||||||
target: { ...target, libcMinimum: '1.2.4' },
|
target: { ...target, libcMinimum: '1.2.4' },
|
||||||
};
|
};
|
||||||
bundle.assertCompatible(manifest, target, '20.20.2');
|
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(
|
assert.throws(
|
||||||
() => bundle.assertCompatible(manifest, { ...target, arch: 'armv7' }),
|
() => bundle.assertCompatible(manifest, { ...target, arch: 'armv7' }),
|
||||||
/platform mismatch/,
|
/platform mismatch/,
|
||||||
@@ -309,7 +338,18 @@ test('publishing requires every platform and validates all immutable archive has
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const index = releaseIndex(f.directory, sourceCommit, repository);
|
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(
|
await fs.appendFile(
|
||||||
path.join(f.directory, `qinglong-${sourceCommit}-${platforms[0]}.tar.gz`),
|
path.join(f.directory, `qinglong-${sourceCommit}-${platforms[0]}.tar.gz`),
|
||||||
'corrupt',
|
'corrupt',
|
||||||
|
|||||||
Reference in New Issue
Block a user