| name | pack-guix |
| description | Auto-generate Guix package definitions from binary URLs and source repositories. Handles FHS compatibility for precompiled binaries, generates build scripts, and supports continuous testing. Triggers: "guix package", "打包 guix", "create guix package", "guix define", "guix 打包", "binary to guix". |
Pack-Guix — 自动 Guix 打包 Skill
从预编译 binary 链接和源码仓库链接自动生成 Guix package 定义。
核心能力:源码分析 → hash 计算 → package.scm 生成 → FHS 兼容处理 → 构建验证 → 持续测试。
工作流
0. 创建隔离工作目录(避免并行 agent 抢夺 /tmp 路径)
1. 接收输入(binary URL + repo URL)
2. 下载并分析 binary(patchelf --print-interpreter / --print-needed)
3. 克隆仓库分析构建系统(CMake/Meson/Makefile/Gradle 等)
4. 计算 hash(guix hash / sha256sum)
5. 生成 package.scm
6. 处理 FHS 兼容性(patchelf 或 wrapper)
7. 构建验证(guix build)
8. Lint 检查(guix lint,强制)
9. 运行时验证(guix shell + 功能测试)
10. 生成测试脚本
⚠ 工作目录隔离(步骤 0,必须先做)
并行运行多个 pack-guix agent 时,固定路径(如 /tmp/pack-guix-binary)会被互相覆盖,导致 hash 错误、解压失败、文件类型异常等诡异问题。因此每次会话必须先创建一个唯一的工作目录,后续所有临时文件(下载的 binary、解压目录、源码 clone)都放在该目录下。
PKG_NAME="${PKG_NAME:-$(basename "<REPO_URL>" .git)}"
WORK_DIR=$(mktemp -d "/tmp/pack-guix-${PKG_NAME}-XXXXXX")
echo "工作目录: $WORK_DIR"
关键:下载 binary 后立即校验文件类型(file 命令)。如果预期是 .deb/tar.gz 却得到 gzip compressed data 或大小明显不符,极可能是工作目录被别的进程覆盖——这种情况在用了 $WORK_DIR 隔离后就不会再发生,但若仍出现,说明 $WORK_DIR 变量未生效,需检查。
输入格式
pack-guix --binary <URL> --repo <URL> [--name <pkg-name>] [--test-cmd <cmd>]
| 参数 | 必需 | 说明 |
|---|
--repo | 是 | 源码仓库链接(GitHub/GitLab/Codeberg) |
--binary | 否 | 预编译 binary 下载链接(tar.gz/zip/AppImage) |
--name | 否 | 包名(默认从 repo 名推断) |
--test-cmd | 否 | 测试命令(默认 <pkg-name> --version) |
注意:
- 假如没有传入
--binary 参数,则尝试从源码构建
- 假如传入了
--binary 参数,则需要在包名后面加一个 -bin 后缀
步骤详解
(如果传入了 binary 参数)步骤 1 :下载分析 Binary
curl -fsSL -o "$WORK_DIR/binary" "<BINARY_URL>"
file "$WORK_DIR/binary"
if file "$WORK_DIR/binary" | grep -qE 'gzip|zip|tar|archive'; then
EXTRACT_DIR="$WORK_DIR/extract"
mkdir -p "$EXTRACT_DIR"
if [[ "$BINARY_URL" == *.tar.gz ]] || [[ "$BINARY_URL" == *.tgz ]]; then
tar xzf "$WORK_DIR/binary" -C "$EXTRACT_DIR"
elif [[ "$BINARY_URL" == *.zip ]]; then
unzip -q "$WORK_DIR/binary" -d "$EXTRACT_DIR"
elif [[ "$BINARY_URL" == *.deb ]]; then
(cd "$EXTRACT_DIR" && ar x "$WORK_DIR/binary" && tar xf data.tar.*)
else
tar xf "$WORK_DIR/binary" -C "$EXTRACT_DIR" 2>/dev/null || \
unzip -q "$WORK_DIR/binary" -d "$EXTRACT_DIR" 2>/dev/null
fi
BINARY_DIR="$EXTRACT_DIR"
else
BINARY_DIR="$WORK_DIR"
fi
find "$BINARY_DIR" -type f -executable | while read bin; do
echo "=== $bin ==="
file "$bin"
patchelf --print-interpreter "$bin" 2>/dev/null || echo "(no interpreter)"
patchelf --print-needed "$bin" 2>/dev/null || echo "(no NEEDED)"
done
关键判断:
- 如果 binary 需要
/lib64/ld-linux-x86-64.so.2 → 需要 FHS 兼容处理
- 记录所有
NEEDED 库 → 映射到 Guix 包
注意:
- 即使
NEEDED 为空,也要警惕:Rust/C++ 二进制可能通过 dlopen 在运行时加载 native addon(如 .node、.so),这些 addon 可能依赖 libgcc_s.so.1 → 需要 gcc 的 lib 子输出(写法见步骤 4 的 input label 规范:("gcc:lib" ,gcc "lib"))
步骤 2:分析源码仓库
git clone --depth 1 "<REPO_URL>" "$WORK_DIR/src"
cd "$WORK_DIR/src"
if [ -f CMakeLists.txt ]; then
BUILD_SYSTEM="cmake"
elif [ -f meson.build ]; then
BUILD_SYSTEM="meson"
elif [ -f Makefile ] || [ -f GNUmakefile ]; then
BUILD_SYSTEM="make"
elif [ -f configure.ac ] || [ -f configure.in ]; then
BUILD_SYSTEM="autoconf"
elif [ -f Cargo.toml ]; then
BUILD_SYSTEM="cargo"
elif [ -f package.json ]; then
BUILD_SYSTEM="npm"
elif [ -f go.mod ]; then
BUILD_SYSTEM="go"
elif [ -f build.gradle ] || [ -f build.gradle.kts ]; then
BUILD_SYSTEM="gradle"
elif [ -f pyproject.toml ] || [ -f setup.py ]; then
BUILD_SYSTEM="python"
else
BUILD_SYSTEM="unknown"
fi
步骤 3:计算 Hash
guix hash <binary>
sha256sum <binary> | xxd -r -p | base32 | tr '[:lower:]' '[:upper:]' | sed 's/=*$//'
SOURCE_URL="<REPO_URL>/archive/refs/tags/v<VERSION>.tar.gz"
curl -fsSL -o "$WORK_DIR/source.tar.gz" "$SOURCE_URL"
guix hash "$WORK_DIR/source.tar.gz"
sha256sum "$WORK_DIR/source.tar.gz" | xxd -r -p | base32 | tr '[:lower:]' '[:upper:]' | sed 's/=*$//'
步骤 4:生成 Package Definition
参见 references/package-template.scm。
Binary 包(raw ELF 或单文件下载):
(define-public <package-name>
(package
(name "<name>")
(version "<version>")
(source (origin
(method url-fetch)
(uri (string-append
"<binary-url-prefix>"
"v" version "/<binary-file-name>"))
(sha256 (base32 "<hash>"))))
(build-system gnu-build-system)
(arguments
(list
#:tests? #f
#:validate-runpath? #f
#:strip-binaries? #f
#:modules '((guix build gnu-build-system)
(guix build utils))
#:phases
#~(modify-phases %standard-phases
(delete 'configure)
(delete 'build)
(replace 'unpack
(lambda _
(copy-file #$source "<binary-name>")
(chmod "<binary-name>" #o755)))
(replace 'install
<ld-linux-wrapper-or-patchelf>))))
(inputs `(("bash-minimal" ,bash-minimal)
("glibc" ,glibc)
("gcc:lib" ,gcc "lib")))
(home-page "<home-page>")
(synopsis "<one-line description>")
(description "<detailed description>")
(license <license>)
(supported-systems '("x86_64-linux"))))
归档 binary 包(tar.gz 含多个文件):
(define-public <package-name>
(package
(name "<name>")
(version "<version>")
(source (origin
(method url-fetch)
(uri (string-append
"<binary-url-prefix>"
"v" version "/<archive-name>.tar.gz"))
(sha256 (base32 "<hash>"))))
(build-system copy-build-system)
(arguments
(list
#:tests? #f
#:validate-runpath? #f
#:strip-binaries? #f
#:install-plan
'("(" ("bin/<binary-name>" "bin/<pkg-name>"))
...))
...))
源码构建包:
(define-public <package-name>
(package
(name "<name>")
(version "<version>")
(source (origin
(method git-fetch)
(uri (git-reference
(url "<repo-url>")
(commit (string-append "v" version))))
(file-name (git-file-name name version))
(sha256 (base32 "<hash>"))))
(build-system <build-system>)
(inputs `(<dependencies>)) ; quasiquote alist,如 ("dep" ,dep)
(home-page "<home-page>")
(synopsis "<one-line description>")
(description "<detailed description>")
(license <license>)))
注意:
- Binary 包用
url-fetch + gnu-build-system(raw ELF)或 copy-build-system(归档)
- 源码包用
git-fetch + 对应的 build-system
步骤 5:FHS 兼容处理
预编译 binary 的必需构建参数:
(arguments
(list
#:tests? #f ; 无源码,不运行测试
#:validate-runpath? #f ; 跳过 RUNPATH 验证(binary 依赖 FHS ld-linux)
#:strip-binaries? #f ; 避免 strip 破坏嵌入资源
...))
方案 A:patchelf(推荐用于从源码构建的包)
(add-after 'install 'patch-binaries
(lambda* (#:key outputs #:allow-other-keys)
(let ((out (assoc-ref outputs "out")))
(for-each (lambda (bin)
(invoke "patchelf"
"--set-interpreter"
(string-append (assoc-ref inputs "glibc")
"/lib/ld-linux-x86-64.so.2")
bin))
(find-files (string-append out "/bin") ".*")))))
方案 B:ld-linux Wrapper(推荐用于预编译 binary 包)
不修改 binary,通过 Guix 的 ld-linux 动态链接器启动 binary:
(replace 'install
(lambda* (#:key inputs #:allow-other-keys)
(let* ((out #$output)
(bin (string-append out "/bin"))
(libexec (string-append out "/libexec"))
(ld.so (string-append (assoc-ref inputs "glibc")
#$(glibc-dynamic-linker)))
;; 拼接 library-path,可包含多个路径
(lib-path (string-join
(list (string-append (assoc-ref inputs "glibc") "/lib")
(string-append (assoc-ref inputs "gcc") "/lib"))
":")))
(mkdir-p libexec)
(install-file "<binary-name>" libexec)
(mkdir-p bin)
(call-with-output-file (string-append bin "/<pkg-name>")
(lambda (port)
(format port
"#!~a\nexec ~a --argv0 ~a/<binary-name> --library-path ~a ~a/<binary-name> \"$@\"\n"
#$(file-append bash-minimal "/bin/sh")
ld.so
libexec
lib-path
libexec)))
(chmod (string-append bin "/<pkg-name>") #o755))))
注意:
--argv0 确保 binary 通过 argv[0] 自定位时能正确找到自身路径
--library-path 为运行时 dlopen 加载的 native addon(如 .node 文件)提供库搜索路径
- 即使 binary 本身
NEEDED 为空,只要它加载了外部 native addon,就需要包含 gcc 的 lib 路径(提供 libgcc_s.so.1、libstdc++.so.6)
步骤 6:构建验证
guix build -f package.scm --dry-run
guix build -f package.scm
步骤 7:Lint 检查(强制)
构建成功后,必须运行 guix lint 检查包定义质量。修复所有报告的问题后再继续。
guix lint -L modules <package-name>
guix lint -f package.scm
guix lint 检查项包括:
- synopsis:是否为单行、无句号、不以包名开头
- description:是否多行、无冗余措辞("this package provides")
- license:许可证标识是否有效
- home-page:URL 是否可达
- inputs:是否有不必要的依赖
- source:源码 URL 是否有效
- formatting:缩进和格式是否规范
如果 lint 报错,回到步骤 4 修正包定义,然后重新执行步骤 6-7。
步骤 8:运行时验证
guix shell -f package.scm -- <pkg-name> --version
步骤 9:生成测试脚本
参见 references/test-template.sh。
测试脚本包含:
- 包定义加载验证
- 构建测试(dry-run + 实际构建)
- Binary 运行测试
- FHS 兼容性验证(动态链接器检查)
- 持续测试循环(可选)
持续测试
用户要求支持持续测试。生成以下文件:
test.sh — 单次测试
#!/usr/bin/env bash
set -euo pipefail
PACKAGE_FILE="${1:-package.scm}"
TEST_CMD="${2:-<pkg-name> --version}"
echo "=== 加载验证 ==="
guix build -f "$PACKAGE_FILE" --dry-run
echo "=== 构建测试 ==="
guix build -f "$PACKAGE_FILE"
echo "=== 运行测试 ==="
guix shell -f "$PACKAGE_FILE" -- $TEST_CMD
watch-test.sh — 持续测试
#!/usr/bin/env bash
set -euo pipefail
PACKAGE_FILE="${1:-package.scm}"
TEST_CMD="${2:-<pkg-name> --version}"
echo "持续测试模式:监控 package.scm 变化自动重建"
inotifywait -m -e modify -e create -e delete \
--format '%w%f' "$PACKAGE_FILE" | while read changed; do
echo "[$(date '+%H:%M:%S')] 检测到变化: $changed"
echo ">>> 重新构建..."
guix build -f "$PACKAGE_FILE" 2>&1 | tail -5
echo ">>> 运行测试..."
guix shell -f "$PACKAGE_FILE" -- $TEST_CMD 2>&1 | tail -3
echo "--- 完成 ---"
done
依赖映射表
Binary 的 patchelf --print-needed 输出 → Guix 包映射:
| 库名 | Guix 包 |
|---|
| libc.so.6 | glibc |
| libstdc++.so.6 | gcc:lib |
| libm.so.6 | glibc |
| libpthread.so.0 | glibc |
| libdl.so.2 | glibc |
| libz.so.1 | zlib |
| libssl.so / libcrypto.so | openssl |
| libcurl.so.4 | curl |
| libfuse.so.2 | fuse |
| libX11.so.6 | libx11 |
| libGL.so.1 | libglvnd |
| libasound.so.2 | alsa-lib |
| libpulse.so.0 | pulseaudio |
| libfontconfig.so.1 | fontconfig-minimal ⚠ |
| libfreetype.so.6 | freetype |
| libnss3.so | nss |
⚠ 变量名 ≠ 包名(label 陷阱):写 input 的 label 时必须用包的实际 name,而非变量名。已知不符:fontconfig 变量 → name: "fontconfig-minimal";openjdk17 变量 → name: "openjdk"。在 quasiquote alist 里 label 写错会同时触发 lint 警告并让 build-side 的 (assoc-ref inputs "fontconfig") 返回 #f(因为实际 label 是 "fontconfig-minimal")。拿不准就 guix show <var> 查 name: 字段。
输出文件清单
执行完成后产出:
| 文件 | 说明 |
|---|
package.scm | Guix 包定义(核心产出) |
test.sh | 单次测试脚本 |
watch-test.sh | 持续测试脚本 |
BUILD-NOTES.org | 构建笔记(Org mode 格式) |
注意: package.scm 生成并测试完成之后,需要在 modules/jeans/packages 中寻找到一个合适的分类,并将其拼合进去
注意事项
- 许可证检测:从仓库 LICENSE 文件自动检测。闭源/专有软件用
nonfree 许可证,但注意三点:
(guix licenses) 不含 nonfree,它来自 (nonguix licenses) 模块
- 必须额外导入:
#:use-module ((nonguix licenses) #:prefix license:)(与 guix licenses 共用 license: prefix)
nonfree 是带参数的过程,调用形式为 (license (license:nonfree "https://项目URL/")),不能写成裸符号 license:nonfree
- 版本推断:优先使用 git tag,其次 binary URL 路径或文件名中的版本号
- 架构限制:如果 binary 仅 x86_64,添加
(supported-systems '("x86_64-linux"))
- 预编译 binary 包的构建系统选择:
- raw ELF 单文件(如
omp-linux-x64)→ gnu-build-system + replace 'unpack 用 copy-file
- 归档文件(tar.gz / zip / AppImage)→
copy-build-system 或 gnu-build-system + 解压逻辑
- 避免盲目使用
trivial-build-system,它不适合需要复杂 install 阶段的 binary
- FHS 警告:如果检测到
/lib64/ld-linux-x86-64.so.2 依赖,必须在 package.scm 中处理(wrapper 或 patchelf)
- 运行时依赖陷阱:即使
patchelf --print-needed 返回空,运行时通过 dlopen 加载的 native addon(常见:Rust 二进制里的 N-API .node 文件、Electron app 的 .so)仍可能依赖 libgcc_s.so.1 或 libstdc++.so.6。构建成功后务必运行一次完整的功能测试。
- input label 规范(必须遵守以通过
guix lint,全通道统一):
- inputs 字段用 quasiquote alist 形式,不要用现代
(list ...):(inputs `(("pkg-name" ,pkg) ...))。
- 带子输出的 input(如 gcc 的 lib):label 必须是
"name:output",即 ("gcc:lib" ,gcc "lib")。现代 (list ... (,gcc "lib"))只会生成裸"gcc" label,**永远过不了 lint**(check-input-labels要求带 output 的 label =name:output`)。
- label 必须用包的实际
name 字段,不是变量名。已知不符:fontconfig 变量 → name: "fontconfig-minimal";openjdk17 变量 → name: "openjdk"。拿不准就 guix show <var> 查 name:。
- build-side 查询与 label 必须一致:
(assoc-ref inputs "gcc:lib")、(this-package-input "fontconfig-minimal") 等。label 写错会让查询返回 #f,导致构建期 wrong-type-arg: Wrong type (expecting string): #f。
- 详见
AGENTS.md 的「input label 规范」与「变量名 ≠ 包名」陷阱。
#$<symbol> gexp 引用必须是已导出的绑定:(gnu packages gcc) 导出 gcc 但不导出 gcc:lib——写 #$gcc:lib 是未绑定符号。gexp 惰性求值,模块加载期不报错,只在构建时爆,是危险的潜伏 bug。正确写法:#$(this-package-input "gcc:lib") 或 #$(gcc "lib")。
与知识库联动
打包完成后:
kb search "guix package" 检查是否已有类似经验
- 如有新发现(特殊依赖、特殊构建步骤)→
kb add 记录