mcpField
P
package-npm-nix

package-npm-nix

github.com/ypares开发3052 次安装今天更新
代码仓库

关于 package-npm-nix

package-npm-nix 是用于将 npm 上的 CLI 工具打包为 Nix 包的技能,涵盖使用预构建包和从源码构建两种路径。它提供依赖、哈希、元数据及可执行文件处理方面的打包流程与检查项。

主要功能

  1. 01选择预构建包或源码构建路径
  2. 02处理 npm 与源码依赖哈希
  3. 03配置 Nix 包元数据和来源分类
  4. 04处理 shebang、原生模块及可执行文件

适用场景

  1. 01为 npm CLI 工具创建 Nix 包
  2. 02将 Bun 构建的工具打包进 Nix
  3. 03排查依赖哈希或构建错误
  4. 04检查包的可复现性和运行状态

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/ypares/agent-skills --skill package-npm-nix

安装后在 Claude Code 中输入/package-npm-nix确认已加载。

详细介绍

为基于 npm 的 CLI 工具创建 Nix 包,涵盖从 npm registry 获取的预构建包,以及具备适当依赖管理的源码构建。本技能提供在 Nix 环境中获取、构建和打包 JavaScript/TypeScript/Bun 工具的模式。

<quick_start> <pre_built_from_npm> 对于已构建并发布到 npm 的工具(最快的方法):

{
  lib,
  stdenv,
  fetchzip,
  nodejs,
}:

stdenv.mkDerivation rec {
  pname = "tool-name";
  version = "1.0.0";

  src = fetchzip {
    url = "https://registry.npmjs.org/${pname}/-/${pname}-${version}.tgz";
    hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
  };

  nativeBuildInputs = [ nodejs ];

  installPhase = ''
    runHook preInstall

    mkdir -p $out/bin
    cp $src/dist/cli.js $out/bin/tool-name
    chmod +x $out/bin/tool-name

    # Fix shebang
    substituteInPlace $out/bin/tool-name \
      --replace-quiet "#!/usr/bin/env node" "#!${nodejs}/bin/node"

    runHook postInstall
  '';

  meta = with lib; {
    description = "Tool description";
    homepage = "https://github.com/org/repo";
    license = licenses.mit;
    sourceProvenance = with lib.sourceTypes; [ binaryBytecode ];
    maintainers = with maintainers; [ ];
    mainProgram = "tool-name";
    platforms = platforms.all;
  };
}

获取哈希:

nix-prefetch-url --unpack https://registry.npmjs.org/tool-name/-/tool-name-1.0.0.tgz
# Convert to SRI format:
nix hash convert --to sri --hash-algo sha256 <hash-output>

</pre_built_from_npm>

<source_build_with_bun> 对于需要使用 Bun 从源码构建的工具:

{
  lib,
  stdenv,
  stdenvNoCC,
  fetchFromGitHub,
  bun,
  makeBinaryWrapper,
  nodejs,
  autoPatchelfHook,
}:

let
  fetchBunDeps =
    { src, hash, ... }@args:
    stdenvNoCC.mkDerivation {
      pname = args.pname or "${src.name or "source"}-bun-deps";
      version = args.version or src.version or "unknown";
      inherit src;

      nativeBuildInputs = [ bun ];

      buildPhase = ''
        export HOME=$TMPDIR
        export npm_config_ignore_scripts=true
        bun install --no-progress --frozen-lockfile --ignore-scripts
      '';

      installPhase = ''
        mkdir -p $out
        cp -R ./node_modules $out
        cp ./bun.lock $out/
      '';

      dontFixup = true;
      outputHash = hash;
      outputHashAlgo = "sha256";
      outputHashMode = "recursive";
    };

  version = "1.0.0";
  src = fetchFromGitHub {
    owner = "org";
    repo = "repo";
    rev = "v${version}";
    hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
  };

  node_modules = fetchBunDeps {
    pname = "tool-name-bun-deps";
    inherit version src;
    hash = "sha256-BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=";
  };
in
stdenv.mkDerivation rec {
  pname = "tool-name";
  inherit version src;

  nativeBuildInputs = [
    bun
    nodejs
    makeBinaryWrapper
    autoPatchelfHook
  ];

  buildInputs = [
    stdenv.cc.cc.lib
  ];

  buildPhase = ''
    # Verify lockfile match
    diff -q ./bun.lock ${node_modules}/bun.lock || exit 1

    # Copy and patch node_modules
    cp -R ${node_modules}/node_modules .
    chmod -R u+w node_modules
    patchShebangs node_modules
    autoPatchelf node_modules

    export HOME=$TMPDIR
    export npm_config_ignore_scripts=true
    bun run build
  '';

  installPhase = ''
    mkdir -p $out/bin
    cp dist/tool-name $out/bin/tool-name
    chmod +x $out/bin/tool-name
  '';

  dontStrip = true;

  meta = with lib; {
    description = "Tool description";
    homepage = "https://github.com/org/repo";
    license = licenses.mit;
    sourceProvenance = with lib.sourceTypes; [ fromSource ];
    maintainers = with maintainers; [ ];
    mainProgram = "tool-name";
    platforms = [ "x86_64-linux" ];
  };
}

</source_build_with_bun> </quick_start>

**确定构建方式**:

检查 npm 包:

# Download and inspect
nix-prefetch-url --unpack https://registry.npmjs.org/package/-/package-1.0.0.tgz
ls -la /nix/store/<hash>-package-1.0.0.tgz/

如果存在包含已构建文件的 dist/ 目录: → 使用预构建方式(更简单、更快)

如果只有 src/,或 package.json 中包含构建脚本: → 使用源码构建方式

检查 package.json 中的:

  • "bin" 字段:显示提供了哪些可执行文件
  • "type": "module":ES 模块(现代包中很常见)
  • "scripts":构建命令(表示需要从源码构建)
  • 运行时:查看是否有 bun、node 或特定版本要求 </step_1_identify_package_type>

<step_2_fetch_hashes> 获取源码和依赖项的哈希:

对于预构建包:

# Fetch npm tarball
nix-prefetch-url --unpack https://registry.npmjs.org/pkg/-/pkg-1.0.0.tgz
# Output: 1abc... (base32 format)

# Convert to SRI format
nix hash convert --to sri --hash-algo sha256 1abc...
# Output: sha256-xyz...

对于源码构建:

# Get GitHub source hash
nix-prefetch-url --unpack https://github.com/org/repo/archive/v1.0.0.tar.gz

# Get dependencies hash (requires iteration):
# 1. Use lib.fakeHash in fetchBunDeps
# 2. Try to build
# 3. Nix will show expected hash in error
# 4. Update hash and rebuild

</step_2_fetch_hashes>

<step_3_create_package_files> 创建包结构:

mkdir -p packages/tool-name

创建 packages/tool-name/package.nix,其中包含完整的 derivation(参见 quick_start)。

创建 packages/tool-name/default.nix:

{ pkgs }: pkgs.callPackage ./package.nix { }

这种双文件模式使包既可独立使用,也可集成到 flake 中。 </step_3_create_package_files>

<step_4_handle_special_cases> 常见的其他要求:

WASM 文件或其他资源:

installPhase = ''
  mkdir -p $out/bin
  cp $src/dist/cli.js $out/bin/tool
  cp $src/dist/*.wasm $out/bin/  # Copy WASM alongside
  chmod +x $out/bin/tool

  substituteInPlace $out/bin/tool \
    --replace-quiet "#!/usr/bin/env node" "#!${nodejs}/bin/node"
'';

多个可执行文件:

# package.json might have:
# "bin": {
#   "tool": "dist/cli.js",
#   "tool-admin": "dist/admin.js"
# }

installPhase = ''
  mkdir -p $out/bin
  for exe in tool tool-admin; do
    cp $src/dist/$exe.js $out/bin/$exe
    chmod +x $out/bin/$exe
    substituteInPlace $out/bin/$exe \
      --replace-quiet "#!/usr/bin/env node" "#!${nodejs}/bin/node"
  done
'';

meta.mainProgram = "tool";  # Primary command

特定平台的二进制文件:

meta = {
  platforms = [ "x86_64-linux" ];  # Bun-compiled binaries often Linux-only
  # or
  platforms = platforms.all;  # Pure JS works everywhere
};

</step_4_handle_special_cases>

<step_5_test_build> 构建和测试:

# Build
nix build .#tool-name

# Test the binary
./result/bin/tool-name --version
./result/bin/tool-name --help

# Check dependencies (Linux)
ldd ./result/bin/tool-name  # Should show all deps resolved

# Format
nix fmt

# Run flake checks
nix flake check

</step_5_test_build>

<metadata_requirements> <essential_fields> 每个包都必须包含完整的元数据:

meta = with lib; {
  description = "Clear, concise description";
  homepage = "https://project-homepage.com";
  changelog = "https://github.com/org/repo/releases";  # Optional but nice
  license = licenses.mit;  # or licenses.unfree for proprietary
  sourceProvenance = with lib.sourceTypes; [
    fromSource          # Built from source
    # or
    binaryBytecode      # Pre-built JS/TS (npm dist/)
    # or
    binaryNativeCode    # Compiled binaries
  ];
  maintainers = with maintainers; [ ];  # Empty OK for community packages
  mainProgram = "binary-name";
  platforms = platforms.all;  # or specific: [ "x86_64-linux" ]
};

</essential_fields>

<source_provenance_guide> 根据打包内容选择:

  • fromSource:在 derivation 过程中从 TypeScript/源码构建
  • binaryBytecode:从 npm registry 获取的预编译 JS
  • binaryNativeCode:原生二进制文件(Rust、Go、Bun 编译)

这会影响安全审计和可复现性方面的预期。 </source_provenance_guide> </metadata_requirements>

<common_patterns> <shebang_replacement> 始终替换 shebang,以确保可复现性:

# Single file
substituteInPlace $out/bin/tool \
  --replace-quiet "#!/usr/bin/env node" "#!${nodejs}/bin/node"

# Multiple files
find $out/bin -type f -exec substituteInPlace {} \
  --replace-quiet "#!/usr/bin/env node" "#!${nodejs}/bin/node" \;

如果未找到匹配项,--replace-quiet 标志会抑制警告。 </shebang_replacement>

<native_dependencies> 处理原生模块(如 sqlite、sharp):

nativeBuildInputs = [
  bun
  nodejs
  makeBinaryWrapper
  autoPatchelfHook  # Linux: patches ELF binaries
];

buildInputs = [
  stdenv.cc.cc.lib  # Provides libgcc_s.so.1, libstdc++.so.6
];

autoPatchelfIgnoreMissingDeps = [
  "libc.musl-x86_64.so.1"  # Ignore musl if not available
];

在 Linux 上会自动运行 autoPatchelf,为 .so 文件修复 RPATH。 </native_dependencies>

<bun_compiled_binaries> 不要剥离 Bun 编译的可执行文件:

# Bun embeds JavaScript in the binary
dontStrip = true;

剥离会移除嵌入的 JS,导致程序无法运行。 </bun_compiled_binaries>

<checking_tarball_contents> 检查 npm 包结构:

# After nix-prefetch-url
ls -la /nix/store/*-pkg-1.0.0.tgz/

# Common layouts:
# dist/cli.js          → Pre-built, use directly
# dist/index.js        → Main entry, check package.json "bin"
# src/index.ts         → Source only, need to build
# lib/                 → Built CommonJS
# esm/                 → Built ES modules

检查 package.json 以找到正确的入口点。 </checking_tarball_contents> </common_patterns>

<anti_patterns> <avoid_these> 不要这样做:

❌ 硬编码 node 路径:

# Bad
"#!/usr/bin/node"  # Won't work on NixOS

✅ 使用 substituteInPlace:

# Good
substituteInPlace $out/bin/tool \
  --replace-quiet "#!/usr/bin/env node" "#!${nodejs}/bin/node"

❌ 跳过哈希验证:

# Bad - insecure
hash = lib.fakeHash;

✅ 获取实际哈希:

# Good - reproducible and secure
hash = "sha256-actual-hash-here";

❌ 忘记设置可执行权限:

# Bad - won't run
cp $src/dist/cli.js $out/bin/tool

✅ 设置可执行位:

# Good
cp $src/dist/cli.js $out/bin/tool
chmod +x $out/bin/tool

❌ 剥离 Bun 二进制文件:

# Bad - breaks Bun-compiled executables
# (default behavior strips binaries)

✅ 禁用剥离:

# Good
dontStrip = true;

</avoid_these> </anti_patterns>

**错误:“hash mismatch in fixed-output derivation”**

你提供的哈希与 Nix 获取的内容不匹配。

解决方法:

  1. Nix 错误会显示“got: sha256-XYZ...”
  2. 将该哈希复制到 derivation 中
  3. 重新构建

对于 fetchBunDeps,首次出现此情况是预期的——使用错误输出获取正确的哈希。 </hash_mismatch>

<missing_executable> 错误:构建后找不到二进制文件

检查:

# List what was actually built
ls -R result/

# Check package.json "bin" field
cat /nix/store/*-source/package.json | jq .bin

# Check build output location
cat /nix/store/*-source/package.json | jq .scripts.build
构建输出的目录可能与预期不同。

<elf_interpreter_error> 错误:“No such file or directory”(运行二进制文件时,Linux)

二进制文件需要进行 ELF 修补,以处理原生依赖项。

解决方案:

nativeBuildInputs = [
  autoPatchelfHook
];

buildInputs = [
  stdenv.cc.cc.lib
];

对于带有原生扩展的 node_modules:

buildPhase = ''
  cp -R ${node_modules}/node_modules .
  chmod -R u+w node_modules
  autoPatchelf node_modules  # Patch .node files
'';

</elf_interpreter_error>

<bun_lock_mismatch> 错误:“bun.lock mismatch”

源代码中的锁文件与缓存的依赖项不匹配。

发生这种情况的原因:

  • 源代码版本已更新,但依赖项哈希未更新
  • 源代码仓库中存在未提交的锁文件更改

解决方案:

  1. 更新源代码哈希,使其与新版本一致
  2. 将依赖项哈希设为 lib.fakeHash
  3. 执行构建以获取正确的依赖项哈希
  4. 更新依赖项哈希
  5. 重新构建 </bun_lock_mismatch>
在确认软件包完成前:
  • nix build .#package-name 成功
  • ./result/bin/tool --version 可正常运行
  • ./result/bin/tool --help 可正常运行
  • nix flake check 通过
  • meta.description 清晰简洁
  • meta.homepage 指向项目网站
  • meta.license 正确
  • meta.sourceProvenance 与打包内容相符
  • 已设置 meta.mainProgram
  • meta.platforms 与工具适用平台相符
  • 所有哈希均为实际值(没有 lib.fakeHash)
  • Shebang 使用 Nix store 路径,而非 /usr/bin
  • 文件已使用 nix fmt 格式化 </build_checklist>

<testing_on_other_platforms> 如果你只有 Linux 环境,但软件包声明支持 platforms.all:

可以请使用 macOS/ARM 的维护者进行测试,或者:

  • 根据你能够测试的平台,保守地标记支持的平台
  • 在软件包中注明其他平台未经测试
  • 让 CI 或其他贡献者扩展平台支持 </testing_on_other_platforms>

<success_criteria> 打包良好的 npm 工具应具备:

  • 构建干净,不出现警告或错误
  • result/bin/ 中有可正常运行的可执行文件
  • 元数据完整且准确
  • 源代码来源分类正确
  • 所有依赖项均已解析(没有缺失的库)
  • 构建可复现(使用实际哈希,构建期间不访问网络)
  • 遵循 Nix 打包约定(修补 shebang、正确设置阶段) </success_criteria>

评论

支持 Markdown