Rust CLI 发布 NPM 的开发记录🫕

背景🍸

在这篇《解压的艺术:用 Rust 处理 .tar.gz 文件🦀》的结尾中我提到了,我用 Rust 写了一个小工具: ogito,在完成了部分基础功能后,我认为,是时候发布第一个可使用的包了。当然,作为一个 Rust 学习者,同时又有一些 JS 情节的人来说,我想要同时发布 crates 和 npm,以便 JavaScript 生态用户通过 npm install -g ogito 直接使用。

一点建议💡

对于本身就有 npm 发布计划的项目来说,napi-rs可能是更好的选择,简单方便,且不容易出错。但是对于我这种前期没有做好规划,本身技术能力也不算高的人来说,迁移的成本远大于手搓(因为手搓不用看文档hhh)。或者你也可以看这篇文章:Publishing a Rust CLI on npm

我的方案👨‍🍳

接下来是我的方案。项目结构如下:

bash 复制代码
ogito/
├── src/                     # Rust 源码
├── package.json             # 主包描述
├── packages/                # 子包生成目录
│   └── _template.json       # 子包模板
├── script/ 
│   └── script.ts            # 生成脚本
├── npm/               
│	├── install.ts           # postinstall 逻辑
│	└── run.ts               # bin 入口
└── .github/workflows/
    └── npm.yml              # CI/CD

问题的关键就是关键的问题❓

Rust 的优点在于其能够发布多平台适配的版本,ogito 作为一个代码工具当然是要适配多个平台。然而,将所有所有的可执行文件打包在一起发布显然是不可取的,这回导致用户下载到无用的文件,同时也会出现不兼容的风险。为此,我采用 主包 + 子包 的发布策略。

  • 主包通过 optionalDependencies 声明所有子包。
  • NPM 在安装时根据当前 oscpu 字段自动拉取匹配的子包。
  • 主包的 postinstall 钩子负责将子包中的二进制解压到 bin 指定路径。
包类型 包名示例 职责 内容
主包 ogito 仅作入口与依赖声明 无二进制
子包 @ogito/darwin-arm64 提供单一平台二进制 压缩后的可执行文件

手动体验💻

首先,我想要先在本地体验一下打包和发布前的流程。首先,使用cargo build --release --target=<target>构建对应平台的二进制文件,文件会出现在target/<target>/release文件夹下,此时需要通过tar将文件打包,再移动到子包的文件夹中,并且生成对应的package.json文件。这就是需要发布的子包的内容。

json 复制代码
// package.json
{
  "name": "@ogito/{{os}}-{{arch}}",
  "version": "{{version}}",
  "description": "ogito binary for {{os}}-{{arch}}",
  "os": ["{{os}}"],
  "bin": {
    "ogito": "{{bin}}"
  },
  "cpu": ["{{arch}}"],
  "files": ["ogito-{{os}}-{{arch}}.tar.gz"],
  "license": "MIT"
}

如何安装🚚

前面介绍到,当用户通过npm安装文件时,会执行postinstall钩子,这是因为在安装时,会根据不同的系统获取对应的包,而这个子包是一个tar.gz文件,需要解压才可以使用,因此,insall脚本就是对文件进行解包。

ts 复制代码
#!/usr/bin/env node  

// install.ts
import * as tar from 'tar'
import { platform, arch } from 'node:os'
import { createRequire } from 'node:module'
import { fileURLToPath } from 'node:url'
import { dirname } from 'node:path'

const __dirname = dirname(fileURLToPath(import.meta.url))
const mapping: Record<string, string> = {
  'win32 x64': `@ogito/win32-x64`,
  'linux x64': `@ogito/linux-x64`,
  'linux arm64': `@ogito/linux-arm64`,
  'darwin x64': `@ogito/darwin-x64`,
  'darwin arm64': `@ogito/darwin-arm64`,
  'win32 arm64': `@ogito/win32-arm64`
}

const key = `${platform()} ${arch()}`
const pkg = mapping[key]
if (!pkg) throw new Error(`Unsupported platform ${key}`)

const require = createRequire(import.meta.url)
const archive = require.resolve(`${pkg}/ogito-${key.replace(' ', '-')}.tar.gz`)

await tar.x({ file: archive, cwd: __dirname })

最后,当用户执行命令时,会执行run.ts这个文件。

ts 复制代码
#!/usr/bin/env node

import { platform } from 'node:os'
import { spawnSync } from 'node:child_process'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'

const root = dirname(fileURLToPath(import.meta.url))
const exe =
  platform() === 'win32' ? join(root, 'ogito.exe') : join(root, 'ogito')
const { status } = spawnSync(exe, process.argv.slice(2), { stdio: 'inherit' })
process.exit(status ?? 0)

发布📦

如果想要手动发布多个平台的子包,你可以使用 windows、Linux、MacOS 的电脑分别进行发布,当然你也可以选择使用 Github Action。

在 CI 中使用 GitHub Actions 进行并行构建,利用 actions/upload-artifact@v4 将每个平台的压缩包暂存,供后续发布统一下载。然后生成好所有的子包文件内容,就可以执行发布了。

yml 复制代码
name: NPM Release
on:
  push:
    tags:
      - 'v*'
jobs:
  build:
    strategy:
      matrix:
        include:
          - os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
            npm-name: linux-x64
          # 其他平台
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v3
      - name: Install cross-compilation tools (Linux ARM64)
        if: matrix.target == 'aarch64-unknown-linux-gnu'
        run: |
          sudo apt-get update
          sudo apt-get install -y gcc-aarch64-linux-gnu
          echo "CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
          echo "CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++" >> $GITHUB_ENV
          echo "AR_aarch64_unknown_linux_gnu=aarch64-linux-gnu-ar" >> $GITHUB_ENV
          echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
      - name: Install Rust target
        run: rustup target add ${{ matrix.target }}
      - name: Set environment to use vendored OpenSSL
        run: echo "OPENSSL_NO_VENDOR=0" >> $GITHUB_ENV
      - name: Build
        run: cargo build --release --target=${{ matrix.target }}
      - name: Package
        shell: bash
        run: |
          cd target/${{ matrix.target }}/release
          if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
            tar -czf ../../../ogito-${{ matrix.npm-name }}.tar.gz ogito.exe
          else
            tar -czf ../../../ogito-${{ matrix.npm-name }}.tar.gz ogito
          fi
      - name: Upload binary artifact
        uses: actions/upload-artifact@v4
        with:
          name: ogito-binary-${{ matrix.npm-name }}
          path: ogito-${{ matrix.npm-name }}.tar.gz
 # 发布部分

总结🦀

通过主包 + 子包 模式,我们把原本臃肿的跨平台 CLI 拆成轻量、按需的分发流程;既减少了用户下载体积,也降低了维护成本。如果你对 Rust 或 CLI 发布有更多兴趣,欢迎体验 ogito🍸------一个代码克隆工具,期待你的反馈与 PR。

相关推荐
崔庆才丨静觅5 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60615 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅6 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅6 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment7 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅7 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊7 小时前
jwt介绍
前端
爱敲代码的小鱼7 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax