对着银河麒麟桌面系统的公开 apt 归档自举出来的容器环境,用于软件构建、打包与兼容性测试。四个大版本、三个档位、跨三种指令集,公开在 GHCR。
docker run --rm ghcr.io/distrotwin/kylin:v11-devel \
bash -c 'grep -E "^VERSION=" /etc/os-release; ldd --version | head -1; gcc -dumpfullversion'VERSION="银河麒麟桌面操作系统V11"
ldd (Ubuntu GLIBC 2.38-1ok6.9k0.5) 2.38
12.3.0
镜像里没有内核。内核态组件(initramfs、TPM、KYSEC LSM 插件)由假包顶替或干脆不装,systemd 只保留容器内有意义的部分。真机上依赖内核特性、安全模块或硬件的行为,在这里不成立。
所以它适合回答「编出来的东西对不对」,不适合回答「跑起来的系统对不对」。镜像与厂商安装介质之间还有一道包版本上的缝,量到底有多大见镜像等于公开归档,不等于安装介质。
该用它:在 CI 里编出能在麒麟上跑的二进制;验证 .deb 打包与依赖;检查产物需要的 glibc / libstdc++ 符号版本目标系统能否满足;复现只在麒麟上出现的编译问题。
别用它:当生产运行时基础镜像;复现内核相关的行为;当作麒麟系统的完整替代品做验收测试。后面这几件事得用真机或虚拟机。
进容器,写个 A+B,编了跑。
docker run -it --rm ghcr.io/distrotwin/kylin:v11-devel /bin/bash进去以后:
echo '#include <stdio.h>
int main(void){ int a, b; if (scanf("%d %d", &a, &b) != 2) return 1; printf("%d\n", a + b); return 0; }' > ab.c
gcc -O2 -o ab ab.c
echo "3 4" | ./ab
objdump -T ab | grep -oE 'GLIBC_[0-9.]+' | sort -uV | tail -17
GLIBC_2.34
ghcr.io/distrotwin/kylin:<版本>-<档位>
先选版本,按你要交付的目标机器:
| 版本 | 系统 | glibc | GCC | 上游血脉 | 架构 |
|---|---|---|---|---|---|
v11 |
银河麒麟桌面 V11(2603) | 2.38 | 12.3 | openKylin 2.0 | amd64 · arm64 · loong64 |
v10sp1 |
银河麒麟桌面 V10 SP1(2503) | 2.31 | 9.3 | Ubuntu 20.04 | amd64 · arm64 |
v10 |
银河麒麟桌面 V10 原版(10.0) |
2.23 | 5.4 | Ubuntu 16.04 | amd64 · arm64 |
v4 |
银河麒麟桌面 V4(4.0.2) | 2.23 | 5.4 | Ubuntu 16.04 | amd64 · arm64 |
v10 与 v10sp1 不是一代东西。 V10 原版走的是 v4 代码线(Ubuntu 16.04、glibc 2.23、GCC 5.4,代号同为 juniper),SP1 才换到 Ubuntu 20.04 的 glibc 2.31。按版本号顺序想当然会挑错。
不确定目标机器是哪一版,就四个都过一遍,见下面「三个 ABI 世代同时验」。
再选档位,按你要在里面做什么:
| 档位 | 装了什么 | 什么时候用 |
|---|---|---|
micro |
CA 证书、时区、locale、libstdc++ | 跑已经编好的二进制,验证它在目标系统上能不能起来 |
base |
加常用命令行工具、python3、systemd | 平台脚本、集成测试 |
devel |
加 build-essential、pkg-config、dpkg-dev |
编译与打包 |
不带档位后缀的 tag 指向 devel,与 golang:1.21 是完整版的惯例一致;latest 是最高版本的 devel,也就是 v11-devel。
docker pull ghcr.io/distrotwin/kylin:v11-devel
docker pull ghcr.io/distrotwin/kylin:v11 # 同上
docker pull ghcr.io/distrotwin/kylin:v10sp1-base
docker pull ghcr.io/distrotwin/kylin:v10-devel # V10 原版,与 SP1 差一代
docker pull ghcr.io/distrotwin/kylin:v4-micro
docker pull ghcr.io/distrotwin/kylin:latest # = v11-devel编一个程序,并确认它的 ABI 需求
docker run --rm -v "$PWD:/w" -w /w ghcr.io/distrotwin/kylin:v10sp1-devel bash -c '
gcc -O2 -o app app.c
objdump -T app | grep -oE "GLIBC_[0-9.]+" | sort -uV | tail -1
'最后那行是产物需要的最高 glibc 符号版本。它不高于目标机器的 glibc,产物才跑得起来。C++ 产物同理,把 GLIBC_ 换成 GLIBCXX_。
三个 ABI 世代同时验
for v in v4 v10sp1 v11; do
printf '%-8s ' "$v"
docker run --rm -v "$PWD:/w" -w /w "ghcr.io/distrotwin/kylin:$v-devel" \
bash -c 'gcc -O2 -o /tmp/app app.c 2>/dev/null && echo 通过' || echo 失败
done一次改动,三代 ABI 一起过。v4 那档尤其能暴露对新 glibc / 新 C++ 标准的隐式依赖——它是 Ubuntu 16.04 血脉,GCC 5.4 默认还不是 C++14。
在 GitHub Actions 里当构建环境
jobs:
build-for-kylin:
runs-on: ubuntu-latest
container: ghcr.io/distrotwin/kylin:v11-devel-20260902 # 钉住带日期的 tag
steps:
- uses: actions/checkout@v4
- run: make打一个 .deb 并检查依赖
docker run --rm -v "$PWD:/w" -w /w ghcr.io/distrotwin/kylin:v11-devel bash -c '
dpkg-buildpackage -us -uc -b
dpkg-deb -I ../*.deb | grep -E "Depends|Architecture"
'验证一个已有产物能不能在目标系统上起来(用 micro,最接近「装完系统什么都没多装」的状态)
docker run --rm -v "$PWD:/w" -w /w ghcr.io/distrotwin/kylin:v4-micro \
bash -c 'ldd ./app; ./app --version'跑非本机架构(需要宿主注册 binfmt,见下方「本地构建」)
docker run --rm --platform linux/loong64 ghcr.io/distrotwin/kylin:v11-devel \
bash -c 'echo $MACHTYPE; gcc -dumpmachine'交互式排查
docker run --rm -it -v "$PWD:/w" -w /w ghcr.io/distrotwin/kylin:v11-devel bashdocker run --rm ghcr.io/distrotwin/kylin:v11-base cat /etc/os-releaseNAME="Kylin"
FULL_NAME="kylin"
VERSION="银河麒麟桌面操作系统V11"
VERSION_US="Kylin Linux Desktop V11"
ID=kylin
PRETTY_NAME="Kylin V11"
VERSION_ID="v11"
HOME_URL="http://www.kylinos.cn/"
VERSION_CODENAME=kylin
PRODUCT_FEATURES=3
KYLIN_RELEASE_ID="V11"
V10 SP1:
NAME="Kylin"
VERSION="银河麒麟桌面操作系统V10 (SP1)"
VERSION_US="Kylin Linux Desktop V10 (SP1)"
ID=kylin
ID_LIKE=debian
PRETTY_NAME="Kylin V10 SP1"
VERSION_ID="v10"
HOME_URL="http://www.kylinos.cn/"
SUPPORT_URL="http://www.kylinos.cn/support/technology.html"
BUG_REPORT_URL="http://www.kylinos.cn/"
PRIVACY_POLICY_URL="http://www.kylinos.cn"
VERSION_CODENAME=kylin
UBUNTU_CODENAME=kylin
PROJECT_CODENAME=v10sp1
V10 原版——注意 VERSION_ID 与 SP1 完全一样:
NAME="Kylin"
VERSION="V10 (juniper)"
ID=kylin
ID_LIKE=debian
VERSION_ID="v10"
PRETTY_NAME="Kylin V10"
HOME_URL="http://www.kylinos.cn/"
SUPPORT_URL="http://www.kylinos.cn/service.aspx"
BUG_REPORT_URL="http://www.kylinos.cn/"
UBUNTU_CODENAME=juniper
V4:
NAME="Kylin"
VERSION="4.0-2 (juniper)"
ID=kylin
ID_LIKE=debian
PRETTY_NAME="Kylin 4.0-2"
VERSION_ID="4.0-2"
HOME_URL="http://www.kylinos.cn/"
SUPPORT_URL="http://www.kylinos.cn/content/service/service.html"
BUG_REPORT_URL="http://www.kylinos.cn/"
UBUNTU_CODENAME=juniper
VERSION 那行的中文系统名是厂商自己写进去的,没有比它更直接的证明。V4 与 V10 原版那两版还没有中文名,它们共用代号 juniper——那也是这两版同属一条代码线的痕迹。
脚本里判断平台时有三处要留意。
VERSION_ID 分不出 SP。 V10 原版与 V10 SP1 都给 v10,一模一样。要区分只能看 PRETTY_NAME(Kylin V10 对 Kylin V10 SP1),SP1 另有 PROJECT_CODENAME=v10sp1 可用。这两版的 glibc 差着 2.23 与 2.31,认错了产物就送不过去。
ID_LIKE 在 V11 上消失了。 V4、V10、V10 SP1 都声明 debian,V11 不声明,case "$ID_LIKE" in *debian*) 会在 V11 上掉进 default 分支。
容器里 uname 报的是宿主内核,跟麒麟无关。
每个档位同时打这些 tag:
| tag | 性质 |
|---|---|
v11-devel |
滚动,跟随最新构建 |
v11 |
滚动,= v11-devel |
latest |
滚动,= 最高版本的 devel |
v11-devel-20260902 |
按 commit 日期,约定不动 |
v11-devel-20260902-amd64 |
单架构,manifest list 的成员 |
CI 里请用带日期的那个。 滚动 tag 会随重建变化,latest 还会随大版本推进跨越 ABI 边界,把你的构建环境从 glibc 2.31 换成 2.38。
日期取的是发布时仓库 HEAD 的 commit 日期(UTC)而非构建时刻,所以同一个 commit 无论何时重建都是同一个 tag。它锁不住的是上游源:同一 commit 隔一周重建,若麒麟归档更新过包则镜像字节不同而 tag 相同。
要钉死请用 digest。 GHCR 的 tag 本来就可变,日期 tag 是约定而非强制;一天内多次发布会覆盖它,发布日志里会打印每个 tag 的 @sha256:...。
docker buildx imagetools inspect ghcr.io/distrotwin/kylin:v11-devel --format '{{.Manifest.Digest}}'
docker pull ghcr.io/distrotwin/kylin@sha256:<digest>docker inspect --format '{{json .Config.Labels}}' \
ghcr.io/distrotwin/kylin:v11-devel | python3 -m json.tool| label | 内容 |
|---|---|
cn.internal.glibc / libstdcpp / glibcxx |
实测的 ABI 值,来自测试阶段在干净机器上跑出来的结果,不是配置里的期望值 |
cn.internal.arch / tier / suite / build-method |
架构、档位、建自源的哪个 suite、走的哪条构建路径 |
cn.internal.repo-commit / buildkit-commit |
哪份配置与哪份脚本建的(2026-09-03 之前发布的镜像里这一项叫 cn.internal.kylin-commit——那个名字把仓库名写死进了公共 workflow,已改成通用的) |
cn.internal.build-run |
跳回当时的 CI 日志与测试报告 |
org.opencontainers.image.* |
标准溯源字段:源仓库、revision、version |
跨架构时这几个 label 尤其有用,原因见下节。要确认手里两个镜像是不是同一份,只能比 digest——镜像里没有记录源快照的 label,那个值只存在于构建机上。
amd64 与 arm64 由原生 runner 构建;V11 另有 loong64(QEMU 模拟构建,实测 micro 约 3 分钟、devel 约 30 分钟)。
同一个版本号在不同架构下的 ABI 并不一致。
| amd64 / arm64 | loong64 | |
|---|---|---|
| V11 | glibc 2.38-1ok6.9k0.5 · libstdc++ 6.0.32(GLIBCXX_3.4.32) |
glibc 2.38-1ok6.12 · libstdc++ 6.0.33(GLIBCXX_3.4.33) |
| V10 SP1 | glibc 2.31 · gcc 9.3 |
glibc 2.28 · gcc 8.3(Loongnix 血脉) |
两支的差距不是一个量级。V10 SP1 差着一整个工具链世代;V11 的两支编译器同为 GCC 12.3,分叉在运行时——loong64 那支的 libstdc++ 反而更新一档,多出 GLIBCXX_3.4.33 这一级符号。
所以在 amd64 上验过的构建结论不能直接套用到 loong64。这是厂商产品线的现状,本仓库只是如实反映;每个镜像都把实测值写进了 label,docker inspect 一看就知道拉到的是哪一档。
只有 V11 有 LoongArch 镜像。 V10 原版的源里根本没有这个架构(它有 mips64el、i386、armhf,独独没有 loongarch),V4 同样没有。 V10 SP1 的 loongarch64 是旧世界 ABI:它的 glibc 2.28 仍在调用 syscall 79/80(newfstatat / fstat),而 LoongArch 新世界把这两个调用去掉了,上游 QEMU 的 loongarch64 target 因此没有实现。最小 rootfs 实测的表现是打开 libtinfo.so.6、读出 ELF 头后报 Unknown syscall 80 退出 127。没有龙芯补丁版 QEMU 或真机就造不出来,所以直接不列入,免得留一个永远红着的 job。
V11 的 gcc 是厂商的包装脚本,每次调用往 stderr 吐一行。
grep: /CurrentlyBuilding: No such file or directory
/usr/bin/gcc 不是原生 ELF 而是麒麟的 shell 包装,它 grep 一个只存在于厂商构建系统里的 /CurrentlyBuilding 来决定要不要加安全加固选项。镜像里没有这个文件,于是每次调用都报一行。编译本身照常成功,这行噪声可以忽略;但如果你的 CI 把 stderr 当失败信号,需要单独放行。V10 SP1 与 V4 的 gcc 是原生 ELF,没有这个现象。
V4 与 V10 原版各有一项注定红的检查。 这两版同属 Ubuntu 16.04 那一代,比其余版本老一个时代,这类项在各自 conf 的 XFAIL 里声明,报告中标为 🟡 而不计为失败:
elf_broken:它们的 gnupg 会装一个链接libldap的gpgkeys_ldap,而本项目不装 Recommends,ldd必报缺库;ircd 的可选模块m_xt.so同理。真机装机同样如此,镜像是忠实的。这条不是「允许有坏 ELF」的通行证,白名单机制仍在,只是这两个文件的缺库状态与真机一致。
判据是「这个现象在真机上也一样吗」。一样就进 XFAIL,不一样就是真缺陷。
四个版本都不经过 ISO,直接从麒麟的 apt 归档 archive.kylinos.cn/kylin/KYLIN-ALL/ bootstrap。V11 走 mmdebstrap;其余三个走两段式自举,因为宿主 dpkg 写出的 status 它们自带的旧 dpkg 读不了。
suite 与 component 都容易踩,而且规律并不统一:
| 版本 | suite | libc6 所在 component |
|---|---|---|
| V11 | 11.0 |
universe |
| V10 SP1 | 10.1 |
universe |
| V10 原版 | 10.0 |
main |
| V4 | 4.0.2-desktop |
universe |
V10 SP1 另配了两个更新源 10.1-2403-updates 与 10.1-2203-updates(EXTRA_SUITES),理由见上面镜像等于公开归档,不等于安装介质。
V4 必须写 4.0.2-desktop 而不是 4.0.2,因为 debootstrap 会核对请求的 suite 名与 Release 自述是否一致,而 apt 不核对。V10 原版的 10.0 曾被误判为「只是补丁源、没有 libc6」——那是只查了 universe 就下的结论,它的 libc6 在 main,78 个 Priority: required 一个不缺。它的 Label 写着「v10-离在线更新推送」,那说的是分发用途,不代表内容不完整。
每个镜像发布前都在干净机器上装载、真正启动、跑完整检查集——构建阶段的机器状态(builder 容器、本地源、宿主装的包)会掩盖镜像自身的缺陷。最近一轮 27 个镜像、1167 项检查:1155 通过、12 项期望不通过、零异常。测试报告与完整日志按系统打包在每次 run 的 artifact 里,成功与失败的都在。
镜像的每一个包都来自麒麟的公开 apt 归档,而客户手上装的是厂商 respin 的安装介质,两者不是同一批构建。差异用 ISO 盘内的装机清单(casper/filesystem.manifest)与 devel 档逐包比出来,看的时候要把两件事分开:厂商构建号不同是同一份上游代码重新打包,上游版本也不同才是真的落后。
| 版本 | 对照介质 | 共有包 | 版本串不同 | 上游版本也不同 |
|---|---|---|---|---|
| V11 | 2603 (20260228) | 248 | 89 | 2 |
| V10 SP1 | 2503 (20250430) | 252 | 96 | 6 |
V11 按上游版本看跟介质是齐的。 那 2 项是 kytrusted-utils 与 libkytrusted-security,可信计算组件、内核态,本来就不在覆盖范围内。其余 87 项只是厂商重打包的构建号,ca-certificates 的上游日期与介质相同。
V10 SP1 的 6 项是 ca-certificates 加 5 个 KYSEC 包。 KYSEC 那 5 个属于内核态安全模块,同样不在覆盖范围内;ca-certificates 是 20230311 对介质的 20240203,落后 11 个月。这靠的是在基础 suite 10.1 之外另配厂商自己文档里列的两个更新源——10.1 是冻结的发布树,后续构建都放在独立的 -updates suite 里。
于是整个队列里唯一一处非内核态的上游版本差异,就是 V10 SP1 那份 ca-certificates。 python3.8、Qt、GLib、libseccomp2、tzdata 都已与介质同上游版本。
ABI 层面两个版本都对得上。 libstdc++6 与 libgcc-s1 和介质逐字符相同,libc6 只差厂商构建号、上游版本相同。所以「符号版本够不够」「链得上链不上」这类问题,镜像给出的答案和真机一致——这正是这套镜像要回答的问题。
四个版本里只有 V10 SP1 有更新源。 V11、V4 的 -updates 候选全部 404(探测时带一条必然不存在的假 suite 做对照,它同样 404,说明这台主机确实区分真假路径、不是被拦)。V10 原版不需要——它的 suite 10.0 自述 Label: v10-离在线更新推送,本身就是滚动的更新通道。
剩下的差异是材料的边界,不是配置疏漏:厂商 respin 里有一批构建从未推到公开归档。要与某张具体介质完全一致,只有拿那张 ISO 切片,那是另一条路径(统信 UOS 镜像走的就是它,代价是只含依赖闭包内的子集)。
git clone --recurse-submodules https://github.com/distrotwin/kylin.git
cd kylin两条构建路径都要一个 Debian 13 的 builder 容器(selfhost 用它跑阶段一,mmdebstrap 整个跑在里面):
docker build -f buildkit/Dockerfile.builder -t dosbuild-cache buildkit/
docker run -d --name dosb --privileged --init -v "$PWD:/w" dosbuild-cache sleep infinityV10 SP1、V10 原版与 V4 在宿主起,脚本自己进容器完成阶段一,并自己把产物导成镜像(把 DID 换成 v10 或 v4 即可):
sudo ARCH=amd64 ROOT=$PWD DID=v10sp1 buildkit/build/build-selfhost.sh micro base devel
sudo ARCH=amd64 ROOT=$PWD buildkit/test/verify.sh v10sp1这条路径在 rootless docker 上跑不完:这三版都装 makedev,它要 mknod,rootless 下即便 --privileged 也是 Operation not permitted,半配置的 makedev 会卡住后续所有 dpkg --configure。要在本地验这三版,宿主得是常规的 rootful docker。
V11 走 mmdebstrap,必须在容器里跑:宿主 Ubuntu 的 apt 与 mmdebstrap 差了几代,signed-by 的解释不同,在宿主上会以 NO_PUBKEY 告终,尽管 keyring 里就有那把钥匙。而且 build.sh 只落 tarball 不建镜像,导入是单独一步:
sudo ARCH=amd64 ROOT=$PWD buildkit/tools/mk-localrepo.sh v11
docker exec dosb bash -c 'umask 022; ROOT=/w BK=/w/buildkit ARCH=amd64 \
/w/buildkit/build/build.sh v11 micro base devel'
sudo ARCH=amd64 ROOT=$PWD buildkit/build/import.sh v11 micro base devel
sudo ARCH=amd64 ROOT=$PWD buildkit/test/verify.sh v11跨架构构建需要宿主装 qemu-user-static 与 binfmt-support,且 QEMU 要够新:LoongArch 的用户态模拟是 QEMU 7.1 才加的,Ubuntu 22.04 只带 6.2。不要引入 tonistiigi/binfmt 容器,实测它会破坏本来可用的 binfmt 注册。
构建只允许手动触发(workflow_dispatch),因为一次要跑二十来个 job、拉几个 GB。两个开关:
include-loongarch:是否一并构建 V11 的 loong64(默认关,需 QEMU 模拟)publish:测试全绿后是否发布到 GHCR(默认关)
三个阶段严格串行:构建(每个「版本 × 架构 × 档位」一个 job,产物走 artifact)→ 测试(在干净机器上装载、启动、跑完整检查集,出按系统分文件的报告)→ 发布(要求 test 与 report 都成功才执行)。
构建机器码在 distrotwin/buildkit,本仓库以 submodule 引用并钉住 commit。本仓库自己只有三个 distros/*.conf 和一份调用 workflow。
distros/v4.conf v10sp1.conf v11.conf # 各版本的源、suite、包列表、ABI 基线、XFAIL
.github/workflows/build.yml # 调用 buildkit 的可复用 workflow
buildkit/ # submodule:构建、测试、门禁、报告、发布