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。

相关推荐
专注API从业者1 小时前
Python + 淘宝 API 开发:自动化采集商品数据的完整流程
大数据·运维·前端·数据挖掘·自动化
烛阴2 小时前
TypeScript高手密技:解密类型断言、非空断言与 `const` 断言
前端·javascript·typescript
样子20183 小时前
Uniapp 之renderjs解决swiper+多个video卡顿问题
前端·javascript·css·uni-app·html
Nicholas683 小时前
flutterAppBar之SystemUiOverlayStyle源码解析(一)
前端
黑客飓风3 小时前
JavaScript 性能优化实战大纲
前端·javascript·性能优化
emojiwoo5 小时前
【前端基础知识系列六】React 项目基本框架及常见文件夹作用总结(图文版)
前端·react.js·前端框架
张人玉5 小时前
XML 序列化与操作详解笔记
xml·前端·笔记
杨荧5 小时前
基于Python的宠物服务管理系统 Python+Django+Vue.js
大数据·前端·vue.js·爬虫·python·信息可视化
YeeWang6 小时前
🎉 Eficy 让你的 Cherry Studio 直接生成可预览的 React 页面
前端·javascript
gnip6 小时前
Jenkins部署前端项目实战方案
前端·javascript·架构