无 root 权限部署 RPM 软件包:以 KLayout 为例的完整实践
环境:EL8 系发行版 8.10(内核
4.18.0-553.x.el8_10.x86_64)、x86_64、普通用户账户、家目录位于 NFS
目标:部署 KLayout 0.30.12,含 GUI 与 Ruby / Python 脚本引擎
约束:无管理员权限,不可使用sudo,不可修改系统目录
本文记录一次完整的无特权部署过程,包含三轮依赖补齐与一处下载完整性问题的定位。结论置于开头:该方案可行,全程仅写入家目录,但需由部署者自行承担包管理器原本提供的职责。
1. 原理:为何无需管理员权限
1.1 "安装"并非内核层面的概念
操作系统内核中不存在 install() 系统调用,内核亦无"软件已安装"这一状态。dnf install 表面上是一个原子动作,分解后均为常规文件操作:
| 操作 | 作用目标 | 需要 root 的原因 |
|---|---|---|
| 写入文件 | /usr、/etc、/opt |
目录属主为 root |
| 登记元数据 | /var/lib/rpm |
同上 |
| 执行 scriptlet | %pre / %post |
可能调用useradd、systemctl |
| 设置属主与权限 | chown root:root、setuid 位 |
仅 root 可变更他人属主 |
| 刷新缓存 | ldconfig 更新 /etc/ld.so.cache |
写入/etc |
包管理器需要 root,根源在于它选择写入 /usr,而非"安装"这一行为本身具有特权属性。变更写入位置后,特权需求即消失。
1.2 RPM 的文件结构
RPM 文件由两部分构成:
[Header 元数据] 包名、版本、依赖列表、scriptlet、文件清单
[Payload] xz 压缩的 cpio 归档
rpm2cpio 的作用是跳过 header,将 payload 解压至标准输出;cpio -idmu 随后将其展开至当前目录。整个过程等价于读取一个文件并在家目录内写入文件。
deb 包的结构更为直接,其本身即 ar 归档:
ar x foo.deb # debian-binary / control.tar.* / data.tar.*
tar -xf data.tar.xz -C ~/opt # data.tar 即文件树
dpkg-deb -x foo.deb ~/opt/foo # 或一步完成
1.3 程序执行所需的条件
execve() 的全部要求如下:
- 文件对调用者具备 x 权限
- 所在文件系统未挂载
noexec - 内核读取 ELF 头,依
PT_INTERP加载动态链接器/lib64/ld-linux-x86-64.so.2 - 链接器按
DT_NEEDED定位依赖库,搜索顺序为:
DT_RPATH → LD_LIBRARY_PATH → /etc/ld.so.cache → /lib64, /usr/lib64
第 4 步是本方案成立的关键:LD_LIBRARY_PATH 的优先级高于系统缓存,且它是普通环境变量,任何用户均可设置。这是 glibc 的既定设计,其意图在于让非特权用户控制自身进程的库搜索路径。
该设计的例外可作为佐证:当进程为 setuid / setgid(内核置 AT_SECURE 标志)时,ld.so 将忽略 LD_LIBRARY_PATH。因为在该场景下注入库等同于提权。普通程序允许用户控制搜索路径,恰因其不存在需要防护的权限边界——用户仅在以自身权限加载自有库、执行自有代码。
真正需要 root 的是一组具体的内核能力(CAP_CHOWN、CAP_SYS_ADMIN、CAP_SYS_MODULE 等),与"执行程序"无关。本方案未涉及其中任何一项。
1.4 rpm --prefix 的局限
rpm 本身提供用户级安装模式:
rpm --dbpath ~/rpmdb --prefix ~/opt --nodeps -i foo.rpm
但 --prefix 仅对标记为 relocatable 的包有效,即 header 中含 Prefixes 标签者。KLayout 的 RPM 未作此标记,该路径不可用。这也是本文采用 rpm2cpio 方案的原因。
2. 代价与适用边界
2.1 放弃包管理器的后果
绕过包管理器意味着放弃其全部服务,具体代价如下:
- 无依赖解析——最主要的代价。
dnf install可一次性解析并拉取 Qt5、libgit2、rubygems;手工部署只能依靠ldd扫描not found后逐轮补齐。本文的三轮返工均源于此。 - 编译期固化的绝对路径不会自动适配——RPM 以
/usr为前缀构建,程序运行时查找/usr/share/rubygems,而副本位于~/opt/.../usr/share/rubygems。动态库可通过LD_LIBRARY_PATH重定向,但各脚本运行时(Ruby、Python、Perl)各有独立的路径变量,需逐一处理。 - 未登记至 RPM 数据库——
rpm -q无法查询,rpm -e无法卸载,卸载需手工rm -rf。 - scriptlet 全部跳过——
%post中的ldconfig、桌面图标注册、MIME 关联均未执行。KLayout 不依赖这些步骤,其他软件未必如此。 - 无自动安全更新,且无法跨用户共享。
2.2 不适用的场景
下列情形无法通过本方案解决:
- 需要 setuid root 的程序(如
ping、sudo) - 需注册 systemd 服务或加载内核模块的软件
- 安装阶段即需向
/etc、/var写入状态的软件(如数据库类) - 硬编码路径且未提供环境变量覆盖机制的程序——此类只能修改源码后重新编译
- 家目录挂载了
noexec(安全加固的常见配置,可通过mount | grep $HOME确认) - SELinux 处于 enforcing 模式且策略限制从家目录加载共享库
2.3 KLayout 的适配性
KLayout 适用于本方案,原因在于其基本实现了位置无关:可执行文件依 argv[0] 推算自身 share/ 目录,共享库依 LD_LIBRARY_PATH,Qt 插件依 QT_PLUGIN_PATH。唯一未做到位置无关的 Ruby loadpath,可通过 RUBYLIB 覆盖。
其共性特征是:每一处"到何处查找文件"的决策均可由环境变量改写,因此整个目录树迁移至家目录后仍可正常运行。
3. 前置确认
3.1 检查既有安装
计算集群通常已预装此类工具,部署前应先行确认:
which klayout; klayout -v
ls -d /opt/* /usr/local/* 2>/dev/null | head -40
module avail 2>&1 | grep -i -E 'klayout|layout'
ldconfig -p | grep -c libQt5Core # 大于 0 表明系统已有 Qt5
若 module avail 有匹配结果,直接 module load 即可。
3.2 仅需 Python API 的情形
若用途限于脚本化处理版图文件,不需要图形界面,则无需本文方案:
python3 -m pip install --user klayout
官方提供 manylinux wheel,单条命令即可完成。需注意模块名为 klayout.db / klayout.lay,而非 GUI 环境中的 pya。本文方案的 71 MB 包集合仅在确需 GUI 时才有必要。
4. 确定包清单
4.1 主包来源
官方在 https://www.klayout.de/build.html 按发行版提供预编译 RPM。CentOS 8 对应的构建可直接用于同系的 8.10 发行版,ABI 兼容。
# 从 build.html 提取真实链接
curl -s https://www.klayout.de/build.html | grep -oP 'href="\K[^"]*CentOS_8[^"]*\.rpm'
得到 https://www.klayout.org/downloads/CentOS_8/klayout-0.30.12-0.x86_64.rpm。
需注意 https://www.klayout.org/downloads/ 目录本身返回 403,无法列目录,但具体文件可正常下载。因此文件名必须从 build.html 中获取。
4.2 从 RPM 头部读取依赖
Qt5 的子包组合无需推测,可直接读取 header(header 未压缩):
rpm -qpR klayout-0.30.12-0.x86_64.rpm
# 无 rpm 命令时:
strings -a klayout-0.30.12-0.x86_64.rpm | grep -E '^(qt5|ruby|python3)' | sort -u
结果为:
qt5-qtbase qt5-qtmultimedia qt5-qtsvg
qt5-qttools qt5-qttools-devel qt5-qtxmlpatterns
ruby python3
同时可见其链接了 libklayout_QtDesigner.so.0,对应 qt5-qttools-libs-designer。
4.3 版本对齐
Rocky Linux 的镜像支持目录列举,可用于查询精确文件名。应优先选择 el8_10 后缀的版本,与内核保持一致:
BASE=https://dl.rockylinux.org/pub/rocky/8.10
curl -s $BASE/AppStream/x86_64/os/Packages/q/ \
| grep -oP 'href="\Kqt5-qtbase[^"]*x86_64\.rpm' | sort -u | tail -3
初始清单共 12 个包:
| 包 | 字节数 | 作用 |
|---|---|---|
klayout-0.30.12-0.x86_64 |
42,475,876 | 主程序 |
qt5-qtbase-5.15.3-9.el8_10 |
3,807,464 | Qt 核心 |
qt5-qtbase-common-5.15.3-9.el8_10 |
42,224 | Qt 公共文件 |
qt5-qtbase-gui-5.15.3-9.el8_10 |
6,435,652 | Qt GUI |
qt5-qtdeclarative-5.15.3-2.el8 |
4,395,396 | QML,qtmultimedia 依赖 |
qt5-qtmultimedia-5.15.3-1.el8 |
902,336 | 主包声明依赖 |
qt5-qtsvg-5.15.3-2.el8 |
188,532 | 主包声明依赖 |
qt5-qtxmlpatterns-5.15.3-1.el8 |
1,195,392 | 主包声明依赖 |
qt5-qttools-libs-designer-5.15.3-6.el8 |
2,898,232 | libklayout_QtDesigner |
qt5-qttools-libs-help-5.15.3-6.el8 |
198,720 | 同上 |
ruby-libs-2.5.9-116.module+el8.10.0+40366+397889f9 |
3,067,058 | DRC 脚本引擎 |
python3-libs-3.6.8-78.el8_10.rocky.0 |
8,216,648 | Python 绑定 |
该清单尚缺 3 个包,详见第 6 节。
5. 下载阶段的完整性保证
5.1 HTTP 200 不代表传输完成
初次下载采用如下写法:
curl -sL -o "$f" -w "%{http_code}" "$u" # 仅检查状态码
12 个包均返回 200。但主包实际仅下载 25,296,896 字节,而正确大小为 42,475,876 字节,缺失 17 MB。传输中途中断,curl 返回的仍是最初的 200。
HTTP 200 仅表示服务器已开始响应,不表示传输完成。正确做法是与 Content-Length 对账:
rem=$(curl -sIL "$u" | grep -i '^content-length:' | tail -1 | tr -dc '0-9')
curl -sL -C - --retry 5 --retry-delay 3 --retry-all-errors -o "$f" "$u"
loc=$(stat -c %s "$f")
[ "$rem" = "$loc" ] || { echo "截断: $loc / $rem"; exit 1; }
-C - 启用断点续传,配合重试循环可补齐中断的传输。服务端需支持 Accept-Ranges: bytes,本文涉及的两个站点均支持。
5.2 自生成校验和的局限
SHA256 校验未能发现上述问题,因为校验清单是基于残缺文件自身计算的:
==> 校验 SHA256
klayout-0.30.12-0.x86_64.rpm: 成功
自行生成的 checksum 只能验证传输一致性,无法验证来源正确性。 验证来源需依赖上游签名或官方公布的哈希值。
因此本文的脚本额外维护一份 SIZES 清单,先比对字节数、再比对哈希。字节数不符时,"截断"这一诊断信息比"哈希不匹配"更具指向性。
5.3 cpio 流完整性验证
比哈希更进一步的方法是解出 payload,检查 cpio 流是否完整。完整的 cpio 归档必须以 TRAILER!!! 结尾:
rpm2cpio foo.rpm > /tmp/p.cpio && tail -c 1024 /tmp/p.cpio | grep -aq 'TRAILER!!!' \
&& echo "完好" || echo "损坏"
对残缺主包执行该检查,结果为:解出 144,719,872 字节、无 TRAILER!!!、截断点位于 ./usr/lib64/klayout/libklayout_db.so.0.30.12(35 MB 的共享库)内部。完整包则解出 144,720,323 字节、335 个 cpio 条目、TRAILER 存在。
对应的报错为:
==> 解包 12 个 RPM
klayout-0.30.12-0.x86_64.rpm
cpio: 文件非正常终止
cpio: premature end of archive 的字面含义即归档提前结束。定位该问题时,笔者先排查了 ulimit、磁盘配额与 NFS 状态,均正常,耗费了一轮无效排查。此处的经验是:当报错明确指向文件截断时,应优先按该方向验证。
6. 依赖补齐过程
6.1 第一轮:libgit2.so.26
12 个包解包完成后,自检报告:
==> 依赖自检
!! 还缺下面这些共享库:
libgit2.so.26
==> 版本验证
.../usr/bin/klayout: error while loading shared libraries:
libgit2.so.26: cannot open shared object file: No such file or directory
此处与预期不同:原本预计会缺失大量 X11 / xcb 库(libxcb-icccm、libxkbcommon-x11 等),实测系统均已提供,仅缺此一项。libgit2 是 KLayout 0.30 的包管理器(Salt)用于访问 Git 仓库的依赖。
curl -s $BASE/AppStream/x86_64/os/Packages/l/ | grep -oP 'href="\Klibgit2-[^"]*x86_64\.rpm'
# → libgit2-0.26.8-3.el8.x86_64.rpm so.26 与 0.26.x 对应
补充该包时同步读取了它自身的依赖,发现还需 libhttp_parser.so.2,故将 http-parser-2.8.0-9.el8(41 KB)一并纳入,避免再增一轮往返。补包时应先读取新包的 Requires,可减少返工次数。
6.2 第二轮:rpm2archive 参数兼容性
klayout-0.30.12-0.x86_64.rpm
(rpm2archive 失败,回退 rpm2cpio)
python3-libs-...
(rpm2archive 失败,回退 rpm2cpio)
...每个包各报一次
原因是 rpm2archive -n(输出至 stdout)为 rpm 4.15+ 引入的参数,EL8 自带的 rpm 4.14 不支持。更需注意的是,它不会明确报错退出,而是忽略该参数、在 RPM 文件旁生成 .tar 文件,因而目录中多出 12 个 *.rpm.tar 冗余文件。
回退逻辑本身正确(功能等价),但不应对每个包重复输出提示。修正方式为探测一次后记录结果:
if [ "${RPM2ARCHIVE_OK:-unknown}" != "no" ] && command -v rpm2archive >/dev/null 2>&1; then
...
if [ "${RPM2ARCHIVE_OK:-unknown}" = "unknown" ]; then
echo " (rpm2archive 不可用,本机 rpm 版本较旧;改用 rpm2cpio,功能等价)"
fi
RPM2ARCHIVE_OK=no
fi
6.3 第三轮:rubygems 与运行时搜索路径
本轮的依赖自检报告"无缺失",klayout -v 亦正常输出版本号:
==> 依赖自检
无缺失,依赖完整。
==> 版本验证
KLayout 0.30.12
但启动 GUI 时报错:
$ klayout
Traceback (most recent call last):
1: from <internal:gem_prelude>:2:in `<internal:gem_prelude>'
<internal:gem_prelude>:2:in `require': cannot load such file -- rubygems.rb (LoadError)
该现象由两个独立原因叠加导致。
6.3.1 原因一:rubygems.rb 未被安装
该文件位于独立的 rubygems 包中,ruby-libs 仅包含标准库。可验证:
rpm2cpio ruby-libs-*.rpm | cpio -t 2>/dev/null | grep -c rubygems # → 0
而 gem_prelude.rb 编译于 libruby.so 内部,Ruby 解释器启动时必然执行 require 'rubygems.rb',缺失该文件即无法初始化。
6.3.2 原因二:Ruby 的库搜索路径为编译期固化的绝对路径
此点较原因一更为隐蔽:即使将 rubygems 解包至 ~/opt/.../usr/share/rubygems,Ruby 仍仅检索 /usr/share/rubygems。
LD_LIBRARY_PATH 仅作用于动态链接器查找 .so 的过程,不影响 Ruby 自身查找 .rb 文件。后者需通过 RUBYLIB 指定:
export RUBYLIB="$R/share/rubygems:$R/share/ruby:$R/share/ruby/vendor_ruby:$R/lib64/ruby:$R/lib64/ruby/vendor_ruby"
export GEM_PATH="$R/share/gems:$HOME/.klayout/gems"
补充 rubygems-2.7.6.3-116.module+el8.10.0+40366+397889f9(316,930 字节)。需确保 build tag 与 ruby-libs 完全一致(均为 116.module+el8.10.0+40366+397889f9),版本混用会引发难以定位的问题。
这是第 2.1 节所述"编译期固化绝对路径"的具体表现形式。各运行时的对应变量为:Ruby 使用 RUBYLIB,Python 使用 PYTHONPATH / PYTHONHOME,Perl 使用 PERL5LIB。遇到 LoadError、ModuleNotFoundError 一类"文件存在于磁盘但无法加载"的错误时,应优先排查此层。
Python 存在同类风险,但 PYTHONHOME 设置不当会影响系统 python3,故仅在系统确实缺少 3.6 标准库时才设置:
if [ ! -f /usr/lib64/python3.6/os.py ] && [ -f "$ROOT/usr/lib64/python3.6/os.py" ]; then
PY_LINE="export PYTHONHOME=\"\$R\""
fi
6.4 错误类型的判读
三轮返工的报错分别为:
cpio: 文件非正常终止 ← 文件损坏
libgit2.so.26: cannot open shared object file ← ENOENT
cannot load such file -- rubygems.rb (LoadError) ← ENOENT
全部为"无法找到",不含任何 Permission denied。
过程中唯一出现的 Permission denied 来自 zip 格式不保留 Unix 执行位,chmod +x 即可解决——修改自有文件的权限位同样无需 root。
这构成一个有效的判断依据:若失败原因均为 ENOENT 而非 EACCES / EPERM,说明方案在权限模型层面可行,剩余工作仅是补齐文件与校正路径。若确实触及权限边界,首个步骤即会返回 EACCES。
7. 完整脚本
7.1 脚本内容
最终清单含 15 个包,合计 71.2 MB。脚本可重复执行(cpio -idmu 为覆盖式),补包后再次运行即可。
#!/bin/bash
# KLayout 0.30.12 无 root 安装脚本 (EL8 系发行版, x86_64)
#
# 用法: bash install-klayout-nonroot.sh
# 自定义目录: KL_ROOT=$HOME/somewhere bash install-klayout-nonroot.sh
#
# 全程只写 $KL_ROOT 和 $HOME/bin,不需要任何管理员权限。
set -euo pipefail
ROOT="${KL_ROOT:-$HOME/opt/klayout-local}"
PKGDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BINDIR="$HOME/bin"
echo "=============================================="
echo " KLayout 无 root 安装"
echo " 包目录: $PKGDIR"
echo " 安装到: $ROOT"
echo " 启动器: $BINDIR/klayout"
echo "=============================================="
echo
# ---------- 0. 环境检查 ----------
if [ "$(uname -m)" != "x86_64" ]; then
echo "错误: 这些包是 x86_64 的,当前是 $(uname -m)" >&2
exit 1
fi
if ! command -v rpm2cpio >/dev/null 2>&1; then
echo "错误: 找不到 rpm2cpio(rpm 包应该是系统自带的)" >&2
exit 1
fi
# ---------- 1. 校验 ----------
# 先比大小再比哈希: 大小对不上说明传输被截断, 这比哈希不符的信息量更大
if [ -f "$PKGDIR/SIZES" ]; then
echo "==> 校验文件大小"
sz_bad=0
while read -r want name; do
[ -z "$want" ] && continue
if [ ! -f "$PKGDIR/$name" ]; then
echo " 缺失: $name" >&2; sz_bad=1; continue
fi
got=$(stat -c %s "$PKGDIR/$name")
if [ "$got" != "$want" ]; then
echo " 截断: $name 实际 $got / 应为 $want" >&2; sz_bad=1
fi
done < "$PKGDIR/SIZES"
if [ "$sz_bad" != 0 ]; then
echo "错误: 文件不完整,重新传输这几个文件" >&2
exit 1
fi
echo " 全部文件大小正确"
echo
fi
if [ -f "$PKGDIR/SHA256SUMS" ] && command -v sha256sum >/dev/null 2>&1; then
echo "==> 校验 SHA256"
( cd "$PKGDIR" && sha256sum -c SHA256SUMS ) || {
echo "错误: 校验失败,传输过程可能损坏" >&2
exit 1
}
echo
fi
# ---------- 2. 解包 ----------
mkdir -p "$ROOT" "$BINDIR"
extract() {
local rpm="$1" tmp rc
# 优先 rpm2archive: 走 tar, 不经过 cpio, 且解压错误不会被管道吞掉。
# 注意 -n 是 rpm 4.15+ 才有的, EL8 自带 rpm 4.14 不支持 -> 首次探测后记住结果
if [ "${RPM2ARCHIVE_OK:-unknown}" != "no" ] && command -v rpm2archive >/dev/null 2>&1; then
tmp="$(mktemp "${TMPDIR:-/tmp}/klrpm.XXXXXX.tgz")"
if rpm2archive -n "$rpm" > "$tmp" 2>/dev/null && [ -s "$tmp" ]; then
if tar -xf "$tmp" -C "$ROOT" 2>/dev/null; then
rm -f "$tmp"; RPM2ARCHIVE_OK=yes; return 0
fi
fi
rm -f "$tmp"
if [ "${RPM2ARCHIVE_OK:-unknown}" = "unknown" ]; then
echo " (rpm2archive 不可用,本机 rpm 版本较旧;改用 rpm2cpio,功能等价)"
fi
RPM2ARCHIVE_OK=no
fi
# 回退: 先把 cpio 落盘再解, 这样能区分"解压断了"和"cpio 读不懂"
tmp="$(mktemp "${TMPDIR:-/tmp}/klrpm.XXXXXX.cpio")"
rpm2cpio "$rpm" > "$tmp"; rc=$?
if [ $rc -ne 0 ]; then
echo "错误: rpm2cpio 解压失败 (exit $rc): $(basename "$rpm")" >&2
rm -f "$tmp"; return 1
fi
if ! tail -c 1024 "$tmp" | grep -aq 'TRAILER!!!'; then
echo "错误: $(basename "$rpm") 的 cpio 流不完整(缺 TRAILER),这个 RPM 是坏的" >&2
echo " 已解出 $(stat -c %s "$tmp") 字节。请重新下载此包。" >&2
rm -f "$tmp"; return 1
fi
( cd "$ROOT" && cpio -idmu --quiet ) < "$tmp"; rc=$?
rm -f "$tmp"
return $rc
}
shopt -s nullglob
rpms=( "$PKGDIR"/*.rpm )
if [ ${#rpms[@]} -eq 0 ]; then
echo "错误: $PKGDIR 下没有 .rpm 文件" >&2
exit 1
fi
echo "==> 解包 ${#rpms[@]} 个 RPM"
for f in "${rpms[@]}"; do
printf ' %s\n' "$(basename "$f")"
extract "$f"
done
echo
if [ ! -x "$ROOT/usr/bin/klayout" ]; then
echo "错误: 解包后没找到 $ROOT/usr/bin/klayout" >&2
exit 1
fi
# ---------- 3. 生成 wrapper ----------
# 环境变量只在这个脚本里生效,不污染平时的 shell
echo "==> 写入启动器 $BINDIR/klayout"
# Ruby/Python 的标准库搜索路径是编译期写死的绝对路径(/usr/share/ruby 等),
# 不会自动找到解在家目录的副本, 必须显式指过去。
RUBYLIB_VAL="\$R/share/rubygems:\$R/share/ruby:\$R/share/ruby/vendor_ruby:\$R/lib64/ruby:\$R/lib64/ruby/vendor_ruby"
# Python: PYTHONHOME 设错会连累系统 python, 所以只在系统确实缺 3.6 标准库时才设
PY_LINE="# 系统自带 python3.6 标准库, 无需 PYTHONHOME"
if [ ! -f /usr/lib64/python3.6/os.py ] && [ -f "$ROOT/usr/lib64/python3.6/os.py" ]; then
PY_LINE="export PYTHONHOME=\"\$R\""
echo " (系统缺 python3.6 标准库,启动器将设置 PYTHONHOME)"
fi
cat > "$BINDIR/klayout" <<EOF
#!/bin/bash
# 由 install-klayout-nonroot.sh 生成
R="$ROOT/usr"
export LD_LIBRARY_PATH="\$R/lib64/klayout:\$R/lib64\${LD_LIBRARY_PATH:+:\$LD_LIBRARY_PATH}"
export QT_PLUGIN_PATH="\$R/lib64/qt5/plugins\${QT_PLUGIN_PATH:+:\$QT_PLUGIN_PATH}"
export QT_QPA_PLATFORM_PLUGIN_PATH="\$R/lib64/qt5/plugins/platforms"
# Ruby 标准库 + rubygems(KLayout 启动时 gem_prelude 会 require 'rubygems.rb')
export RUBYLIB="$RUBYLIB_VAL\${RUBYLIB:+:\$RUBYLIB}"
export GEM_HOME="\$HOME/.klayout/gems"
export GEM_PATH="\$R/share/gems:\$GEM_HOME"
$PY_LINE
exec "\$R/bin/klayout" "\$@"
EOF
chmod +x "$BINDIR/klayout"
echo
# ---------- 4. 依赖自检 ----------
echo "==> 依赖自检"
export LD_LIBRARY_PATH="$ROOT/usr/lib64/klayout:$ROOT/usr/lib64${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
MISS="$(mktemp)"
{
ldd "$ROOT/usr/bin/klayout" || true
for so in "$ROOT"/usr/lib64/klayout/*.so*; do [ -e "$so" ] && ldd "$so" || true; done
for so in "$ROOT"/usr/lib64/qt5/plugins/*/*.so; do [ -e "$so" ] && ldd "$so" || true; done
} 2>/dev/null | grep 'not found' | awk '{print $1}' | sort -u > "$MISS" || true
if [ -s "$MISS" ]; then
echo " !! 还缺下面这些共享库:"
sed 's/^/ /' "$MISS"
echo
echo " 查属于哪个包: repoquery --whatprovides <库名>"
else
echo " 无缺失,依赖完整。"
fi
rm -f "$MISS"
echo
# ---------- 5. 功能验证 ----------
# -v 只打印版本, 不初始化脚本引擎; 必须真跑一段脚本才能发现 Ruby/Python 的问题
echo "==> 功能验证"
FAIL=0
printf ' %-22s' "版本:"
VER="$("$BINDIR/klayout" -v 2>&1 | head -1)" && echo "$VER" || { echo "失败"; FAIL=1; }
TD="$(mktemp -d "${TMPDIR:-/tmp}/klchk.XXXXXX")"
trap 'rm -rf "$TD"' EXIT
# Ruby 引擎: gem_prelude 会 require rubygems, 这里能过说明 RUBYLIB 配对了
cat > "$TD/t.rb" <<'RB'
puts "OK ruby #{RUBY_VERSION}"
RB
printf ' %-22s' "Ruby 引擎:"
OUT="$(QT_QPA_PLATFORM=offscreen "$BINDIR/klayout" -b -r "$TD/t.rb" 2>&1)" || true
case "$OUT" in
*"OK ruby"*) echo "${OUT##*OK }" | head -1 ;;
*) echo "失败"; echo "$OUT" | sed 's/^/ /' | head -8; FAIL=1 ;;
esac
# Python 引擎
cat > "$TD/t.py" <<'PY'
import sys
print("OK python %s" % sys.version.split()[0])
PY
printf ' %-22s' "Python 引擎:"
OUT="$(QT_QPA_PLATFORM=offscreen "$BINDIR/klayout" -b -r "$TD/t.py" 2>&1)" || true
case "$OUT" in
*"OK python"*) echo "${OUT##*OK python }" | head -1 ;;
*) echo "失败"; echo "$OUT" | sed 's/^/ /' | head -8; FAIL=1 ;;
esac
# 版图内核: 建一个 cell 画个方框再读回来, 确认 GDS 读写链路通
cat > "$TD/t2.rb" <<'RB'
ly = RBA::Layout.new
top = ly.create_cell("TOP")
l1 = ly.layer(1, 0)
top.shapes(l1).insert(RBA::Box.new(0, 0, 1000, 2000))
ly.write(ARGV[0])
ly2 = RBA::Layout.new
ly2.read(ARGV[0])
c = ly2.top_cell
puts "OK gds #{c.name} #{c.bbox.width}x#{c.bbox.height}"
RB
printf ' %-22s' "GDS 读写:"
OUT="$(QT_QPA_PLATFORM=offscreen "$BINDIR/klayout" -b -r "$TD/t2.rb" "$TD/t.gds" 2>&1)" || true
case "$OUT" in
*"OK gds"*) echo "${OUT##*OK gds }" | head -1 ;;
*) echo "失败"; echo "$OUT" | sed 's/^/ /' | head -8; FAIL=1 ;;
esac
echo
if [ "$FAIL" != 0 ]; then
echo "!! 有检查未通过,把上面的报错贴出来即可定位。"
echo
fi
cat <<EOF
==============================================
完成。把 ~/bin 加进 PATH(只需一次):
echo 'export PATH=\$HOME/bin:\$PATH' >> ~/.bashrc
source ~/.bashrc
用法:
klayout design.gds # GUI,需要 ssh -X
klayout -e design.gds # 编辑模式
klayout -b -r script.rb # 批处理
卸载: rm -rf $ROOT $BINDIR/klayout
==============================================
EOF
7.2 成功输出
部署成功时的输出:
==> 校验文件大小
全部文件大小正确
==> 校验 SHA256
(15 个包全部成功)
==> 解包 15 个 RPM
(rpm2archive 不可用,本机 rpm 版本较旧;改用 rpm2cpio,功能等价)
...
==> 写入启动器 /home/<user>/bin/klayout
==> 依赖自检
无缺失,依赖完整。
==> 功能验证
版本: KLayout 0.30.12
Ruby 引擎: ruby 2.5.9
Python 引擎: 3.6.8
GDS 读写: TOP 1000x2000
8. 验证方法:不应仅依据 --version
这是第 6.3 节返工的直接原因。
klayout -v 仅打印版本号,不初始化脚本引擎,因此在 rubygems 缺失的情况下仍输出 KLayout 0.30.12。以该输出判定部署成功,随后启动 GUI 即失败。
通用原则是:自检应覆盖各子系统的真实代码路径。对 KLayout 而言即分别执行一段 Ruby、一段 Python,并实际创建 cell、写入 GDS 后读回,四项均通过方可判定部署完成。上述脚本第 5 节即实现该检查。
更一般地说,--version 通常是程序中最浅的执行路径,往往在主逻辑初始化之前即返回。以其作为部署成功的判据,验证强度接近于无。
9. 使用
9.1 配置 PATH
echo 'export PATH=$HOME/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
9.2 启动图形界面
ssh -X user@host # 需带 -X,本地需 X Server
klayout design.gds
echo $DISPLAY 应有输出(形如 localhost:10.0);若为空则表明转发未建立,需检查服务端 sshd_config 中的 X11Forwarding 配置。
9.3 界面使用注意事项
- 默认为 Viewer 模式,不可编辑——需编辑时使用
klayout -e,或在File → Setup → Application → Editing mode中启用后重启。 - 打开后显示空白——通常为图层未显示。在右侧 Layers 面板右键选择
Add Other Layer Entries。 - 无法定位目标 cell——通过左侧 Cells 面板双击切换,
Display → Show as new top可将子 cell 作为顶层显示。
常用快捷键:* 全局缩放、F2 适配窗口、Ctrl+Shift+H 显示层级树。
10. 经验总结
10.1 可复用的七条结论
- RPM / deb 并非特权格式,payload 即标准压缩归档。使用
rpm2cpio | cpio -idmu或dpkg-deb -x解包至家目录即可。rpm --prefix仅对 relocatable 包有效,多数包不适用。 - HTTP 200 不等于下载完整。应与
Content-Length对账,并配合-C -续传。 - 自行生成的 checksum 只验证传输、不验证来源。额外维护字节数清单,先比大小后比哈希——"截断"的诊断价值高于"哈希不匹配"。
- cpio 流完整性以
TRAILER!!!为判据,相比哈希更能定位具体文件与截断位置。 LD_LIBRARY_PATH仅作用于.so。脚本语言查找自身库文件使用独立机制:RUBYLIB/PYTHONPATH/PERL5LIB。遇到"文件存在但加载失败"类错误应优先排查该层。- 不应仅以
--version验证部署。该路径通常在主逻辑初始化前返回,需覆盖各子系统的真实代码路径。 - 依据错误类型判断方案可行性:全部为 ENOENT 表明属于文件缺失的工程问题;出现 EACCES / EPERM 才是真正的权限边界。本文三轮返工均不含权限类错误。
10.2 方案选择建议
若用途限于脚本化处理版图文件,应优先采用:
python3 -m pip install --user klayout
# 或 micromamba create -n kl -c conda-forge python=3.11 klayout
本文的 15 包 / 71 MB 方案仅在确需图形界面时才具备必要性。
附录:最终包清单(15 个,71.2 MB)
41496 http-parser-2.8.0-9.el8.x86_64.rpm
42475876 klayout-0.30.12-0.x86_64.rpm
464060 libgit2-0.26.8-3.el8.x86_64.rpm
8216648 python3-libs-3.6.8-78.el8_10.rocky.0.x86_64.rpm
3807464 qt5-qtbase-5.15.3-9.el8_10.x86_64.rpm
42224 qt5-qtbase-common-5.15.3-9.el8_10.noarch.rpm
6435652 qt5-qtbase-gui-5.15.3-9.el8_10.x86_64.rpm
4395396 qt5-qtdeclarative-5.15.3-2.el8.x86_64.rpm
902336 qt5-qtmultimedia-5.15.3-1.el8.x86_64.rpm
188532 qt5-qtsvg-5.15.3-2.el8.x86_64.rpm
2898232 qt5-qttools-libs-designer-5.15.3-6.el8.x86_64.rpm
198720 qt5-qttools-libs-help-5.15.3-6.el8.x86_64.rpm
1195392 qt5-qtxmlpatterns-5.15.3-1.el8.x86_64.rpm
316930 rubygems-2.7.6.3-116.module+el8.10.0+40366+397889f9.noarch.rpm
3067058 ruby-libs-2.5.9-116.module+el8.10.0+40366+397889f9.x86_64.rpm

浙公网安备 33010602011771号