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。

相关推荐
程序员鱼皮2 分钟前
前特斯拉 AI 总监:AI 编程最大的谎言,是 “提效”
前端·后端·ai·程序员·开发
pusheng202513 分钟前
普晟传感2026年新春年会总结与分析
前端·javascript·html
谢尔登15 分钟前
React19事件调度的设计思路
前端·javascript·react.js
Emma_Maria25 分钟前
本地项目html和jquery,访问地址报跨域解决
前端·html·jquery
奋斗吧程序媛30 分钟前
常用且好用的命令
前端·编辑器
2301_7965125233 分钟前
【精通篇】打造React Native鸿蒙跨平台开发高级复合组件库开发系列:Lazyload 懒加载(懒加载的图片)
前端·javascript·react native·react.js·ecmascript·harmonyos
敲敲了个代码39 分钟前
从N倍人力到1次修改:Vite Plugin Modular 如何拯救多产品前端维护困境
前端·javascript·面试·职场和发展·typescript·vite
Yff_world1 小时前
网络安全与 Web 基础笔记
前端·笔记·web安全
Sapphire~1 小时前
Vue3-19 hooks 前端数据和方法的封装
前端·vue3
浩宇软件开发1 小时前
基于OpenHarmony鸿蒙开发医院预约挂号系统(前端后端分离)
前端·华为·harmonyos